Official TypeScript/JavaScript client for the Ad Context Protocol (AdCP). Build distributed advertising operations that work synchronously OR asynchronously with the same code.
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. 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/client/docs/ after install.
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.
npm install @adcp/clientimport { ADCPMultiAgentClient } from '@adcp/client';
// Configure agents and handlers
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',
},
],
{
// Webhook URL template (macros: {agent_id}, {task_type}, {operation_id})
webhookUrlTemplate: 'https://myapp.com/webhook/{task_type}/{agent_id}/{operation_id}',
// Activity callback - fires for ALL events (requests, responses, status changes, webhooks)
onActivity: activity => {
console.log(`[${activity.type}] ${activity.task_type} - ${activity.operation_id}`);
// Log to monitoring, update UI, etc.
},
// Status change handlers - called for ALL status changes (completed, failed, input-required, working, etc)
handlers: {
onGetProductsStatusChange: (response, metadata) => {
// Called for sync completion, async webhook, AND status changes
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') {
// Handle clarification needed
console.log('Needs input:', metadata.message);
}
},
},
}
);
// Execute operation - library handles operation IDs, webhook URLs, context management
const agent = client.agent('agent_x');
const result = await agent.getProducts({ brief: 'Coffee brands' });
// onActivity fired: protocol_request
// onActivity fired: protocol_response
// Check result
if (result.status === 'completed') {
// Agent completed synchronously!
console.log('✅ Sync completion:', result.data.products.length, 'products');
// onGetProductsStatusChange handler ALREADY fired with status='completed' ✓
}
if (result.status === 'submitted') {
// Agent will send webhook when complete
console.log('⏳ Async - webhook registered at:', result.submitted?.webhookUrl);
// onGetProductsStatusChange handler will fire when webhook arrives ✓
}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);
// onActivity fired: status_change (input-required)
// Continue the conversation with the same agent
const refined = await agent.continueConversation('Only premium brands above $50');
// onActivity fired: protocol_request
// onActivity fired: protocol_response
if (refined.status === 'completed') {
console.log('✅ Got refined results:', refined.data.products.length);
// onGetProductsStatusChange handler fired ✓
}
}All webhooks (task completions AND notifications) use one endpoint with flexible URL templates.
const client = new ADCPMultiAgentClient(agents, {
// Path-based (default pattern)
webhookUrlTemplate: 'https://myapp.com/webhook/{task_type}/{agent_id}/{operation_id}',
// OR query string
webhookUrlTemplate: 'https://myapp.com/webhook?agent={agent_id}&op={operation_id}&type={task_type}',
// OR custom path
webhookUrlTemplate: 'https://myapp.com/api/v1/adcp/{agent_id}?operation={operation_id}',
// OR namespace to avoid conflicts
webhookUrlTemplate: 'https://myapp.com/adcp-webhooks/{agent_id}/{task_type}/{operation_id}',
});// Handles ALL webhooks (task completions and notifications)
app.post('/webhook/:task_type/:agent_id/:operation_id', async (req, res) => {
const { task_type, agent_id, operation_id } = req.params;
// Route to agent client - handlers fire automatically
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 });
});const operationId = createOperationId();
const webhookUrl = agent.getWebhookUrl('sync_creatives', operationId);
// Returns: https://myapp.com/webhook/sync_creatives/agent_x/op_123
// (or whatever your template generates)Get observability into everything happening:
const client = new ADCPMultiAgentClient(agents, {
onActivity: activity => {
console.log({
type: activity.type, // 'protocol_request', 'webhook_received', etc.
operation_id: activity.operation_id,
agent_id: activity.agent_id,
status: activity.status,
});
// Stream to UI, save to database, send to monitoring
eventStream.send(activity);
},
});Activity types:
protocol_request- Request sent to agentprotocol_response- Response received from agentstatus_change- Task status changedwebhook_received- Webhook received from agent
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.
// When creating a media buy, agent registers for delivery notifications
const result = await agent.createMediaBuy({
campaign_id: 'camp_123',
budget: { amount: 10000, currency: 'USD' },
// Agent internally sets up recurring delivery_report notifications
});
// Later, agent sends notifications to your webhook
const client = new ADCPMultiAgentClient(agents, {
handlers: {
onMediaBuyDeliveryNotification: (notification, metadata) => {
console.log(`Report #${metadata.sequence_number}: ${metadata.notification_type}`);
// notification_type indicates progress:
// 'scheduled' → Progress update (like status: 'working')
// 'final' → Operation complete (like status: 'completed')
// 'delayed' → Still waiting (extended timeline)
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
Full TypeScript support with IntelliSense:
// All responses are fully typed
const result = await agent.getProducts(params);
// result: TaskResult<GetProductsResponse>
if (result.success) {
result.data.products.forEach(p => {
console.log(p.name, p.price); // Full autocomplete!
});
}
// Handlers receive typed responses
handlers: {
onCreateMediaBuyStatusChange: (response, metadata) => {
// response: CreateMediaBuyResponse | CreateMediaBuyAsyncWorking | ...
// metadata: WebhookMetadata
if (metadata.status === 'completed') {
const buyId = (response as CreateMediaBuyResponse).media_buy_id; // Typed!
}
};
}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/client';
function handleCreateMediaBuy(rawParams: unknown): CreateMediaBuyResponse {
const request: CreateMediaBuyRequest = CreateMediaBuyRequestSchema.parse(rawParams);
// request.buyer_ref, request.account, request.brand — all typed
}Note: PackageRequest (creation-shaped, required fields) differs from Package (response-shaped). See the type catalog for all request types and their required fields.
Execute across multiple agents simultaneously:
const client = new ADCPMultiAgentClient([agentX, agentY, agentZ]);
// Parallel execution across all agents
const results = await client.allAgents().getProducts({ brief: 'Coffee brands' });
// results: TaskResult<GetProductsResponse>[]
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}`);
}
});const client = new ADCPMultiAgentClient(agents, {
webhookSecret: process.env.WEBHOOK_SECRET,
});
// Signatures verified automatically on handleWebhook()
// Returns 401 if signature invalidconst agents = [
{
id: 'agent_x',
name: 'Agent X',
agent_uri: 'https://agent-x.com',
protocol: 'a2a',
auth_token: process.env.AGENT_X_TOKEN, // ✅ Secure - load from env
},
];# .env
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"
}
]'// Auto-discover from environment
const client = ADCPMultiAgentClient.fromEnv();All AdCP tools with full type safety:
Media Buy Lifecycle:
getProducts()- Discover advertising productslistCreativeFormats()- Get supported creative formatscreateMediaBuy()- Create new media buyupdateMediaBuy()- Update existing media buysyncCreatives()- Upload/sync creative assetslistCreatives()- List creative assetsgetMediaBuyDelivery()- Get delivery performance
Audience & Targeting:
getSignals()- Get audience signalsactivateSignal()- Activate audience signalsprovidePerformanceFeedback()- Send performance feedback
Protocol:
getAdcpCapabilities()- Get agent capabilities (v3)
Build agent registries by discovering properties agents can sell. Works with AdCP v2.2.0's publisher-domain model.
- Agents return publisher domains: Call
listAuthorizedProperties()→ getpublisher_domains[] - Fetch property definitions: Get
https://{domain}/.well-known/adagents.jsonfrom each domain - Index properties: Build fast lookups for "who can sell X?" and "what can agent Y sell?"
import { PropertyCrawler, getPropertyIndex } from '@adcp/client';
// First, crawl agents to discover properties
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();
// Query 1: Who can sell this property?
const matches = index.findAgentsForProperty('domain', 'cnn.com');
// Returns: [{ property, agent_url, publisher_domain }]
// Query 2: What can this agent sell?
const auth = index.getAgentAuthorizations('https://agent-x.com');
// Returns: { agent_url, publisher_domains: [...], properties: [...] }
// Query 3: Find by tags
const premiumProperties = index.findAgentsByPropertyTags(['premium', 'ctv']);import { PropertyCrawler, getPropertyIndex } from '@adcp/client';
const crawler = new PropertyCrawler();
// Crawl agents - gets publisher_domains from each, then fetches adagents.json
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`);
// Now query
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}`);
}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.
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.
Simple unified event log for all operations:
CREATE TABLE webhook_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
operation_id TEXT NOT NULL, -- Groups related events
agent_id TEXT NOT NULL,
task_type TEXT NOT NULL, -- 'sync_creatives', 'media_buy_delivery', etc.
status TEXT, -- For tasks: 'submitted', 'working', 'completed'
notification_type TEXT, -- For notifications: 'scheduled', 'final', 'delayed'
sequence_number INTEGER, -- For notifications: report sequence
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);
-- Query all events for an operation
SELECT * FROM webhook_events
WHERE operation_id = 'op_123'
ORDER BY timestamp;
-- Get all delivery reports for agent + month
SELECT * FROM webhook_events
WHERE operation_id = 'delivery_report_agent_x_2025-10'
ORDER BY sequence_number;For development and testing, use the included CLI tool to interact with AdCP agents.
Save agents for quick access:
# Save an agent with an alias
npx @adcp/client --save-auth test https://test-agent.adcontextprotocol.org
# Use the alias
npx @adcp/client test get_products '{"brief":"Coffee brands"}'
# List saved agents
npx @adcp/client --list-agentsAuto-detect protocol and call directly:
# Protocol auto-detection (default)
npx @adcp/client https://test-agent.adcontextprotocol.org get_products '{"brief":"Coffee"}'
# Force specific protocol with --protocol flag
npx @adcp/client https://agent.example.com get_products '{"brief":"Coffee"}' --protocol mcp
npx @adcp/client https://agent.example.com list_authorized_properties --protocol a2a
# List available tools
npx @adcp/client https://agent.example.com
# Use a file for payload
npx @adcp/client https://agent.example.com create_media_buy @payload.json
# JSON output for scripting
npx @adcp/client https://agent.example.com get_products '{"brief":"..."}' --json | jq '.products'Three ways to provide auth tokens (priority order):
# 1. Explicit flag (highest priority)
npx @adcp/client test get_products '{"brief":"..."}' --auth your-token
# 2. Saved in agent config (recommended)
npx @adcp/client --save-auth prod https://prod-agent.com
# Will prompt for auth token securely
# 3. Environment variable (fallback)
export ADCP_AUTH_TOKEN=your-token
npx @adcp/client test get_products '{"brief":"..."}'# Save agent with auth
npx @adcp/client --save-auth prod https://prod-agent.com mcp
# List all saved agents
npx @adcp/client --list-agents
# Remove an agent
npx @adcp/client --remove-agent test
# Show config file location
npx @adcp/client --show-config# Run test scenarios against an agent
npx @adcp/client test test-mcp full_sales_flow
npx @adcp/client test test-mcp --list-scenarios
# Run compliance assessment
npx @adcp/client comply test-mcp
npx @adcp/client comply test-mcp --platform-type social_platform
npx @adcp/client comply --list-platform-typesProtocol 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.
Install the AdCP CLI as a Claude Code plugin to use /adcp-client:adcp directly in your AI coding assistant:
# Add the marketplace (one time)
/plugin marketplace add adcontextprotocol/adcp-client
# Install the plugin
/plugin install adcp-client@adcpOr test locally during development:
claude --plugin-dir ./path/to/adcp-clientTry the live testing UI at http://localhost:8080 when running the server:
npm startFeatures:
- 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)
const result = await agent.getProducts({ brief: 'Coffee brands' });const result = await agent.createMediaBuy(
{ buyer_ref: 'campaign-123', account_id: 'acct-456', packages: [...] },
(context) => {
// Agent needs more info
if (context.inputRequest.field === 'budget') {
return 50000; // Provide programmatically
}
return context.deferToHuman(); // Or defer to human
}
);const operationId = createOperationId();
const result = await agent.syncCreatives(
{ creatives: largeCreativeList },
null, // No clarification handler = webhook mode
{
contextId: operationId,
webhookUrl: agent.getWebhookUrl('sync_creatives', operationId),
}
);
// Result will be 'submitted', webhook arrives later
// Handler fires when webhook receivedThe examples above show client-side usage — calling existing agents. To build your own agent that serves AdCP tools:
import { createTaskCapableServer, taskToolResponse, GetSignalsRequestSchema } from '@adcp/client';
const server = createTaskCapableServer('My Signals Agent', '1.0.0');
server.tool('get_signals', 'Discover audience segments.', GetSignalsRequestSchema.shape, async args => {
const signals = queryYourDatabase(args.signal_spec);
return taskToolResponse({ signals, sandbox: true }, `Found ${signals.length} segment(s)`);
});See the Build an Agent guide for the full walkthrough, and examples/signals-agent.ts for a complete runnable example.
Contributions welcome! See CONTRIBUTING.md for guidelines.
Apache 2.0 License - see LICENSE file for details.
- Documentation: docs.adcontextprotocol.org
- Issues: GitHub Issues
- Protocol Spec: AdCP Specification