
Security News
PolinRider: North Korea-Linked Supply Chain Campaign Expands Across Open Source Ecosystems
PolinRider expands across npm, Packagist, Go modules, and Chrome extensions, using hidden loaders to target developer environments.
AdCP SDK — client, server, and compliance harnesses for the AdContext Protocol (MCP + A2A)
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. 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.
AdCP operations are distributed and asynchronous by default. An agent might:
Your code stays the same. You write handlers once, and they work for both sync completions and webhook deliveries.
npm install @adcp/sdk@adcp-3.0 # 7.x, AdCP 3.0
npm install @adcp/sdk@adcp-3.1 # 8.x beta, 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.
@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.
| Need | Recommended import |
|---|---|
| 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.
import { ADCPMultiAgentClient } from '@adcp/sdk';
// 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 agentMental 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? 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; // → ACCOUNT_NOT_FOUND
return db.findAccount(id, ctx);
},
},
sales: defineSalesCorePlatform({
getProducts: async (req, ctx) => ({ products: catalog.search(req) }), // req typed ✓
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.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.AccountStore shapes: InMemoryImplicitAccountStore (Shape A — buyer-driven sync_accounts), createOAuthPassthroughResolver (Shape B — vendor OAuth + /me/adaccounts), createRosterAccountStore (Shape C — publisher-curated roster).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);
// request.buyer_ref, request.account, request.brand — all typed
}
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.
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}`);
}
});
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 });
// Key used on the wire (auto-generated or caller-supplied). Log alongside your own IDs.
result.metadata.idempotency_key;
// true when the response was a cached replay. Side-effecting callers MUST gate
// notifications, memory writes, downstream calls on this flag.
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) {
// Agent re-planned with a different payload. Mint a fresh key and retry.
}
if (result.errorInstance instanceof IdempotencyExpiredError) {
// Key past the seller's replay window. Look up by natural key before retrying.
}
BYOK (persist keys across process restarts so crash-recovery can resend the exact key):
import { useIdempotencyKey } from '@adcp/sdk';
// Validates against the spec pattern `^[A-Za-z0-9_.:-]{16,255}$` before the round-trip.
const key = await db.getOrCreateIdempotencyKey(campaign.id);
await client.createMediaBuy({ ...params, ...useIdempotencyKey(key) });
// Check the seller's replay window so you know when to fall back to natural-key lookup.
// Throws on v3 sellers that omit the declaration — the SDK does NOT default to 24h.
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.
const client = new ADCPMultiAgentClient(agents, {
webhookSecret: process.env.WEBHOOK_SECRET,
});
// Signatures verified automatically on handleWebhook()
// Returns 401 if signature invalid
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
# Publish public-jwks.json at your /.well-known/jwks.json endpoint.
# Point to it from your /.well-known/brand.json agents[].jwks_uri.
Sign outbound requests (buyer):
import { createSigningFetch } from '@adcp/sdk/signing';
const signingFetch = createSigningFetch(fetch, {
keyid: 'my-agent-2026',
alg: 'ed25519',
privateKey: privateJwk, // JWK with `d` field
});
await signingFetch('https://seller.example.com/mcp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
// Signature, Signature-Input, and Content-Digest headers added automatically.
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
);
// On verify: req.verifiedSigner = { keyid, agent_url?, verified_at }.
// On reject: 401 with WWW-Authenticate: Signature error="<code>".
Full guide covering key generation, JWKS publication, brand.json setup, webhook signing, capability declaration, key rotation, and conformance testing: docs/guides/SIGNING-GUIDE.md.
const 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 performanceAudience & Targeting:
getSignals() - Get audience signalsactivateSignal() - Activate audience signalsprovidePerformanceFeedback() - Send performance feedbackProtocol:
getAdcpCapabilities() - Get agent capabilities (v3)Build agent registries by discovering properties agents can sell. Works with AdCP v2.2.0's publisher-domain model.
listAuthorizedProperties() → get publisher_domains[]https://{domain}/.well-known/adagents.json from each domainimport { PropertyCrawler, getPropertyIndex } from '@adcp/sdk';
// 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/sdk';
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:
PropertyCrawlerLibrary provides discovery logic - you add persistence layer.
adagents.json CatalogsUse 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.
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/sdk@adcp-3.0 --save-auth test https://test-agent.adcontextprotocol.org
# Use the alias
npx @adcp/sdk@adcp-3.0 test get_products '{"brief":"Coffee brands"}'
# List saved agents
npx @adcp/sdk@adcp-3.0 --list-agents
Auto-detect protocol and call directly:
# Protocol auto-detection (default)
npx @adcp/sdk@adcp-3.0 https://test-agent.adcontextprotocol.org get_products '{"brief":"Coffee"}'
# Force specific protocol with --protocol flag
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
# List available tools
npx @adcp/sdk@adcp-3.0 https://agent.example.com
# Use a file for payload
npx @adcp/sdk@adcp-3.0 https://agent.example.com create_media_buy @payload.json
# JSON output for scripting
npx @adcp/sdk@adcp-3.0 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/sdk@adcp-3.0 test get_products '{"brief":"..."}' --auth your-token
# 2. Saved in agent config (recommended)
npx @adcp/sdk@adcp-3.0 --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/sdk@adcp-3.0 test get_products '{"brief":"..."}'
# Save agent with auth
npx @adcp/sdk@adcp-3.0 --save-auth prod https://prod-agent.com mcp
# List all saved agents
npx @adcp/sdk@adcp-3.0 --list-agents
# Remove an agent
npx @adcp/sdk@adcp-3.0 --remove-agent test
# Show config file location
npx @adcp/sdk@adcp-3.0 --show-config
# Run test scenarios against an agent
npx @adcp/sdk@adcp-3.0 test test-mcp full_sales_flow
npx @adcp/sdk@adcp-3.0 test test-mcp --list-scenarios
# Run compliance assessment
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.
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@adcp
Or test locally during development:
claude --plugin-dir ./path/to/adcp-client
Try the live testing UI at http://localhost:8080 when running the server:
npm start
Features:
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 received
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:
| Skill | For | Storyboard |
|---|---|---|
skills/build-seller-agent/ | Publishers, SSPs, retail media | media_buy_seller |
skills/build-generative-seller-agent/ | AI ad networks, generative DSPs | media_buy_generative_seller |
skills/build-signals-agent/ | CDPs, data providers | signal_owned, signal_marketplace |
skills/build-retail-media-agent/ | Retail media networks | media_buy_catalog_creative |
skills/build-creative-agent/ | Ad servers, creative platforms | creative_lifecycle |
For manual implementation, see the Build an Agent guide and examples/signals-agent.ts.
Contributions welcome! See CONTRIBUTING.md for guidelines.
Apache 2.0 License - see LICENSE file for details.
FAQs
AdCP SDK — client, server, and compliance harnesses for the AdContext Protocol (MCP + A2A)
The npm package @adcp/sdk receives a total of 4,066 weekly downloads. As such, @adcp/sdk popularity was classified as popular.
We found that @adcp/sdk demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
PolinRider expands across npm, Packagist, Go modules, and Chrome extensions, using hidden loaders to target developer environments.

Security News
Open source attacks are accelerating as AI coding agents pull in dependencies faster, with less human review.

Research
/Security News
Malicious Chrome and Firefox extensions posed as free VPNs while stealing clipboard data through later extension updates.