adcp-client
Adds the /adcp skill for calling AdCP advertising agent tools over MCP or A2A, running protocol compliance scenarios, and querying the AdCP
Open source Open in the app JSON README (API)
About
Adds the /adcp skill for calling AdCP advertising agent tools over MCP or A2A, running protocol compliance scenarios, and querying the AdCP registry. Includes built-in test agents for zero-config use.
Details
- Kind
- Plugins
- Topic
- AI, RAG & memory
- Publisher
- adcontextprotocol
- Origin
- marketplace
- Category
- ferramentas
- Stars
- 18
- Forks
- 16
- Open pull requests
- 1
- Last push
- 2026-09-07T15:28:17Z
- Repository state
- ativo
- Language
- TypeScript
- License
- Apache-2.0
- Added
- 2026-08-30 01:48:58
- Updated
- 2026-08-30 01:48:58
- Origin id
adcontextprotocol/adcp-client/adcp-client
README
# @adcp/sdk
[](https://badge.fury.io/js/@adcp%2Fsdk)
[](https://www.npmjs.com/package/@adcp/sdk)
[](https://www.apache.org/licenses/LICENSE-2.0)
[](https://www.typescriptlang.org/)
[](https://adcontextprotocol.github.io/adcp-client/api/)
[](https://github.com/adcontextprotocol/adcp-client/actions)
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`](./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`](./docs/guides/BUILD-AN-AGENT.md). **Calling** an AdCP agent as a buyer? Load [`skills/call-adcp-agent/SKILL.md`](./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`](./docs/guides/SIGNING-GUIDE.md). For type signatures, use [`docs/TYPE-SUMMARY.md`](./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
```bash
npm install @adcp/sdk@adcp-3.0 # 7.x, AdCP 3.0
npm install @adcp/sdk # 13.x, maintained AdCP 3.1 stable line
npm install '@adcp/sdk@^14.0.0-0' # newest 14.x prerelease, AdCP 3.2 beta
```
Trying the v14 prerelease? Read the [14.0.0 prerelease notes](./docs/releases/14.0.0-beta.0.md), then use the [13-to-14](./docs/migration-13-to-14.md) or [12-to-14](./docs/migration-12-to-14.md) migration guide. The npm `latest` tag remains on v13 for the maintained AdCP 3.1 stable line. SDK 14 requires Node.js `^20.19.0 || >=22.12.0`. Older paths: [12-to-13](./docs/migration-12-to-13.md), **[MIGRATION-v8.md](./MIGRATION-v8.md)**, and [8.0-to-8.1](./docs/migration-8.0-to-8.1.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:
```ts
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`.
### TypeScript footprint in monorepos
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.
## Quick Start: AdCP 3.2
SDK 14 makes the compact 3.2 lifecycle primary:
```text
list_products → buy_products → control_media_buy
↘ request_proposals → refine_proposals → accept_proposal
```
Start with the persona guide that matches your job:
- [Build a seller](./docs/guides/SELLER-QUICKSTART-3.2.md)
- [Call a seller](./docs/guides/BUYER-QUICKSTART-3.2.md)
- [Upgrade from SDK 13](./docs/migration-13-to-14.md)
- [Run in production](./docs/guides/PRODUCTION-DURABILITY.md)
```typescript
import { ADCPMultiAgentClient } from '@adcp/sdk';
const client = ADCPMultiAgentClient.simple('https://seller.example/mcp/', {
authToken: process.env.ADCP_TOKEN,
});
const seller = client.agent('default-agent');
const listed = await seller.listProducts({
account: { account_id: 'account-42' },
brand: { domain: 'advertiser.example' },
});
if (listed.status === 'completed') {
console.log(listed.data.products, listed.data.feed_version);
}
```
The established `get_products` / `create_media_buy` / `update_media_buy`
surface remains supported for AdCP 3.0/3.1 compatibility. Its detailed client
patterns follow below.
## Established lifecycle and distributed operations
```typescript
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');
// Products expose canonical format_options[]; legacy named-format refs do not
// cross the primary AgentClient boundary.
console.log(result.data.products[0]?.format_options[0]?.format_kind);
// 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 ✓
}
```
### Handling Clarifications (input-required)
When an agent needs more information, you can continue the conversation:
```typescript
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 ✓
}
}
```
## Webhook Pattern
All webhooks (task completions AND notifications) use one endpoint with flexible URL templates.
### Configure Your Webhook URL Structure
```typescript
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}',
});
```
### Single Webhook Endpoint
```typescript
// 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 });
});
```
### URL Generation is Automatic
```typescript
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)
```
## Activity Events
Get observability into everything happening:
```typescript
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 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.
```typescript
// 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`
## Type Safety
Full TypeScript support with IntelliSense:
```typescript
// 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!
}
};
}
```
### 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.
```typescript
import { serve } from '@adcp/sdk';
import {
createAdcpServerFromPlatform,
createIdempotencyStore,
definePlatform,
defineSalesCorePlatform,
memoryBackend,
refAccountId,
} from '@adcp/sdk/server';
// Single-process example. Use pgBackend(pool) or redisBackend(client) for
// durable, replica-safe production replay.
const idempotency = createIdempotencyStore({ backend: memoryBackend(), ttlSeconds: 86400 });
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', idempotency }));
```
`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`](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):
```typescript
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`](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`](docs/migration-5.x-to-6.x.md). Note: `PackageRequest` (creation-shaped, required fields) differs from `Package` (response-shaped). See the [type catalog](docs/ZOD-SCHEMAS.md#type-catalog) for all request types and their required fields.
## Multi-Agent Operations
Execute across multiple agents simultaneously:
```typescript
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}`);
}
});
```
## 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.
```typescript
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:
```typescript
import { IdempotencyConflictError, IdempotencyExpiredError } from '@adcp/sdk';
if (result.errorInstance instanceof IdempotencyConflictError) {
// Reconcile by natural key before deciding whether this is a new intent.
// Do not blindly mint a fresh key: the prior operation may have succeeded.
}
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):
```typescript
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`](./docs/guides/idempotency-crash-recovery.md).
## Security
### Webhook Signature Verification
```typescript
const client = new ADCPMultiAgentClient(agents, {
webhookSecret: process.env.WEBHOOK_SECRET,
});
// Signatures verified automatically on handleWebhook()
// Returns 401 if signature invalid
```
### Request Signing (RFC 9421)
AdCP 3.0 supports [HTTP Message Signatures (RFC 9421)](https://www.rfc-editor.org/rfc/rfc9421) 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:**
```bash
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):**
```typescript
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):**
```typescript
import { createExpressVerifier, StaticJwksResolver, InMemoryReplayStore } from '@adcp/sdk/signing';
// Raw-body capture MUST be mounted ahead of the verifier — express.json()
// would otherwise consume the stream and the verifier would have no bytes to
// hash. `rawBodyVerify` comes from `createExpressAdapter()`; the inline form is
// equivalent.
app.use(express.json({ verify: adapter.rawBodyVerify }));
// or: app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf.toString('utf8'); } }));
app.post(
'/mcp',
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](./docs/guides/SIGNING-GUIDE.md)**.
### Authentication
```typescript
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
},
];
```
## Environment Configuration
```bash
# .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"
}
]'
```
```typescript
// Auto-discover from environment
const client = ADCPMultiAgentClient.fromEnv();
```
## Available Tools
Primary AdCP tools have typed request and response maps. Less-common or
extension tools use `executeCustomTask<T>()` explicitly.
**Media Buy Lifecycle:**
The primary client surface speaks canonical creatives: products use
`format_options[]`, packages select `format_option_refs[]`, and creatives use
`format_kind` plus an optional `format_option_ref`. The SDK performs negotiated
legacy-wire translation internally. Migration tooling can opt into the clearly
named `createMediaBuyLegacy()`, `updateMediaBuyLegacy()`,
`syncCreativesLegacy()`, `listCreativesLegacy()`, `buildCreativeLegacy()`, and
`previewCreativeLegacy()` escape hatches. `executeCustomTask()` cannot invoke
typed primary tasks or legacy creative tools; other less-common standard and
extension tools intentionally use that explicit raw-task route.
- `getProducts()` - Discover advertising products
- `listCreativeFormatsLegacy()` - Inspect the legacy named-format catalog (migration tooling only)
- `createMediaBuy()` - Create new media buy
- `updateMediaBuy()` - Update existing media buy
- `syncCreatives()` - Upload/sync creative assets
- `listCreatives()` - List creative assets
- `buildCreativeLegacy()` - Build through the legacy named-format protocol (migration tooling only)
- `previewCreative()` - Preview by advertised capability, inline manifest, or creative-library ID
- `previewCreativeLegacy()` - Preview through the legacy named-format protocol (migration tooling only)
- `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
1. **Agents return publisher domains**: Call `listAuthorizedProperties()` → get `publisher_domains[]`
2. **Fetch property definitions**: Get `https://{domain}/.well-known/adagents.json` from each domain
3. **Index properties**: Build fast lookups for "who can sell X?" and "what can agent Y sell?"
### Three Key Queries
```typescript
import { 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']);
```
### Full Example
```typescript
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}`);
}
```
### 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 relationship verification
Use `RegistryClient.lookupBrand()` to resolve a domain and verify its relationship to a house. The public v3 registry does not expose an ordered-chain endpoint; v3 hierarchy is one level deep.
```ts
import { RegistryClient } from '@adcp/sdk';
const registry = new RegistryClient();
const brand = await registry.lookupBrand('leaf.example', { fresh: true });
const verifiedHouse =
brand && !brand.live_brand_json && (brand.relationship_trust === 'mutual' || brand.relationship_trust === 'inline')
? brand.house_domain
: undefined;
```
Only `relationship_trust: "mutual"` and `"inline"` are reciprocated. For `mutual`, `relationship_verified_at` says when both sides were last observed agreeing. `claimed_house_domain` is a unilateral leaf claim and must not be used for authorization. `ResolvedBrand.parent_brand` is a registry reference that may be a portfolio-internal id, not a portable traversal API.
`source` answers a different question: where the selected identity record came from. Provenance is not relationship authorization, and callers must not infer a relationship from `source`. Treat an absent `relationship_trust` as unknown, not `standalone`. Pass `{ fresh: true }` when a live origin check is required; if `live_brand_json` is present, that check failed and the response came from stored evidence, so a strict live-evidence policy must reject it. Policies that permit stored evidence should apply their own age ceiling to `relationship_verified_at` and `relationship_declared_at`. If `promoted_from_schema` is present, inspect every `migration_warnings` entry. Until the registry guarantees a warning for every discarded legacy field, absence of a warning is not evidence that a legacy relationship was promoted.
### Community Mirror `adagents.json` Catalogs
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.
```ts
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:
```typescript
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:
```sql
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;
```
## 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:
```bash
# 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
```
### Direct URL Usage
Auto-detect protocol and call directly:
```bash
# 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'
```
### Authentication
Three ways to provide auth tokens (priority order):
```bash
# 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":"..."}'
```
### Agent Management
```bash
# 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
```
### Testing & Compliance
```bash
# 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](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:
```bash
# Add the marketplace (one time)
/plugin marketplace add adcontextprotocol/adcp-client
# Install the plugin
/plugin install adcp-client@adcp
```
Or test locally during development:
```bash
claude --plugin-dir ./path/to/adcp-client
```
## Testing
Try the live testing UI at `http://localhost:8080` when running the server:
```bash
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
```typescript
const result = await agent.getProducts({ brief: 'Coffee brands' });
```
### With Clarification Handler
```typescript
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
}
);
```
### With Webhook for Long-Running Operations
```typescript
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
```
## 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:
```bash
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/`](skills/build-seller-agent/SKILL.md) | Publishers, SSPs, retail media | `media_buy_seller` |
| [`skills/build-generative-seller-agent/`](skills/build-generative-seller-agent/SKILL.md) | AI ad networks, generative DSPs | `media_buy_generative_seller` |
| [`skills/build-signals-agent/`](skills/build-signals-agent/SKILL.md) | CDPs, data providers | `signal_owned`, `signal_marketplace` |
| [`skills/build-retail-media-agent/`](skills/build-retail-media-agent/SKILL.md) | Retail media networks | `media_buy_catalog_creative` |
| [`skills/build-creative-agent/`](skills/build-creative-agent/SKILL.md) | Ad servers, creative platforms | `creative_lifecycle` |
For manual implementation, see the [Build an Agent guide](docs/guides/BUILD-AN-AGENT.md) and [`examples/signals-agent.ts`](examples/signals-agent.ts).
## Contributing
Contributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
## License
Apache 2.0 License - see [LICENSE](LICENSE) file for details.
## Support
- **Documentation**: [docs.adcontextprotocol.org](https://docs.adcontextprotocol.org)
- **Issues**: [GitHub Issues](https://github.com/adcontextprotocol/adcp-client/issues)
- **Protocol Spec**: [AdCP Specification](https://github.com/adcontextprotocol/adcp)