This document contains essential guidelines for AI coding assistants (Claude, Copilot, etc.) working on the AdCP Client project.
Protocol overview — Read docs/llms.txt for a single-file summary of AdCP: all 46 tools, key types, error codes, common flows, and test scenarios. Also available at https://adcontextprotocol.github.io/adcp-client/llms.txt
Type reference — Read docs/TYPE-SUMMARY.md for curated type signatures (AgentConfig, TaskResult, ConversationContext, and all tool request/response shapes).
Do NOT read these files — they are large generated files that waste context:
src/lib/types/tools.generated.ts(~13,000 lines) — use TYPE-SUMMARY.md insteadsrc/lib/types/core.generated.ts(~2,000 lines) — use TYPE-SUMMARY.md insteadsrc/lib/types/schemas.generated.ts(~8,000 lines) — Zod runtime schemas, rarely needed directlysrc/lib/agents/index.generated.ts— generated Agent classes, use the client API instead
Building a server-side agent? — Read docs/guides/BUILD-AN-AGENT.md and the published compliance storyboards at https://adcontextprotocol.org/compliance/{version}/ (mirrored locally in compliance/cache/{version}/ after npm run sync-schemas).
Testing an agent for conformance? — Read docs/guides/CONFORMANCE.md and call runConformance(agentUrl, opts) from @adcp/sdk/conformance. Property-based fuzzing against the bundled JSON schemas; stateless tier covers 11 discovery tools across every protocol.
Migration guides — docs/migration-6.6-to-6.7.md (fifteen adopter recipes; two breaking changes — 'implicit' enforces inline-account_id refusal and SalesPlatform split into SalesCorePlatform & SalesIngestionPlatform; plus definePlatform helpers, composeMethod, accounts.resolve security presets, accounts.upsert/list/syncGovernance(ctx), typed errors, issues[].hint, BuyerAgentRegistry, createTenantRegistry, createOAuthPassthroughResolver, createRosterAccountStore, createTenantStore w/ built-in tenant gate, MEDIA_BUY_TRANSITIONS lifecycle helpers, createMediaBuyStore for targeting_overlay echo, NoAccountCtx narrow, refAccountId(ref)); docs/migration-5.x-to-6.x.md (5 → 6 framework shape); docs/migration-4.x-to-5.x.md (4 → 5); docs/migration-buyer-agent-registry.md (registry deep-dive).
Putting credentials in ctx_metadata? — Don't. Read docs/guides/CTX-METADATA-SAFETY.md — wire-strip protects buyer responses but not log lines / error envelopes / heap dumps / adopter strings. Re-derive bearers per request; embed only non-secret upstream IDs.
@adcp/sdk is the official TypeScript client library for the Ad Context Protocol (AdCP), documented at docs.adcontextprotocol.org.
Components:
- Library (
src/lib/) - NPM package for AdCP agent communication - CLI (
bin/) - Command-line tooling for testing agents
- A2A Protocol: ALWAYS use the official
@a2a-js/sdkclient - MCP Protocol: ALWAYS use the official
@modelcontextprotocol/sdkclient - NEVER implement custom HTTP fallbacks or protocol implementations
- NEVER parse SSE responses manually
- NEVER make direct fetch() calls to agent endpoints
- If an official client fails to import, FIX THE IMPORT - don't create workarounds
- NEVER inject mock products, formats, or any other fake data
- NEVER provide fallback data when agents return empty responses
- ALWAYS return exactly what the agents provide
- If an agent returns empty arrays or errors, show that to the user
- Real data only - no exceptions
The MCP SDK automatically handles initialization. Authentication must be provided via headers:
const transport = new StreamableHTTPClientTransport(url, {
requestInit: {
headers: {
'x-adcp-auth': authToken,
},
},
});
await client.connect(transport); // This automatically calls initialize internallyThe library supports both A2A and MCP protocols via unified interface:
import { ProtocolClient } from './src/lib/protocols';
// Routes to: callA2ATool() or callMCPTool() based on agent.protocol🚨 CRITICAL: A2A Protocol Implementation Requirements
The A2A protocol has specific implementation requirements that differ from MCP:
1. Artifact Field Names
A2A artifacts use artifactId per @a2a-js/sdk Artifact interface, NOT name:
// Correct (per @a2a-js/sdk)
if (!artifact.artifactId) {
warnings.push('A2A artifact missing artifactId field');
}
// Incorrect - 'name' doesn't exist in A2A SDK
if (!artifact.name) { ... }2. Two Types of Webhooks - Do Not Confuse!
1. push_notification_config - For Async Task Status Updates
Used for receiving task completion/progress notifications. Placement differs by protocol:
-
A2A Protocol: Goes in
params.configuration.pushNotificationConfig(camelCase)await a2aClient.sendMessage({ message: { /* task content */ }, configuration: { pushNotificationConfig: { // ← For async task status url: webhookUrl, token?: clientToken, authentication: { schemes: ['HMAC-SHA256'], credentials: secret } } } });
-
MCP Protocol: Goes in tool arguments as
push_notification_config(snake_case)await mcpClient.callTool('create_media_buy', { buyer_ref: '...', packages: [...], push_notification_config: { // ← For async task status url: webhookUrl, token?: clientToken, authentication: { schemes: ['HMAC-SHA256'], credentials: secret } } });
2. reporting_webhook - For Reporting Data Delivery
Used for receiving periodic performance metrics. Always stays in skill parameters (both A2A and MCP):
// Both protocols - reporting_webhook in skill parameters
{
buyer_ref: '...',
packages: [...],
reporting_webhook: { // ← Stays in parameters for BOTH protocols
url: reportingUrl,
token?: clientToken,
authentication: { schemes: ['HMAC-SHA256'], credentials: secret },
reporting_frequency: 'daily', // Additional fields specific to reporting
requested_metrics: ['impressions', 'spend', 'clicks']
}
}Schema: https://adcontextprotocol.org/schemas/v1/core/push-notification-config.json
The ProtocolClient.callTool() method in src/lib/protocols/index.ts handles this routing automatically.
All operations follow 5 status patterns based on agent response:
completed- Sync completion with resultworking- Long-running, poll withtasks/getsubmitted- Webhook delivery requiredinput_required- Agent needs clarification via input handlersdeferred- Client defers decision to human/external system
// Core flow in src/lib/core/TaskExecutor.ts
const result = await executor.executeTask(agent, 'get_products', params, inputHandler);
// Status determines next steps: polling, webhook wait, or input handlingAgents may need clarifications during execution. Use input handlers:
const client = new AdCPClient(agent, {
handlers: {
onGetProductsStatusChange: (response, metadata) => {
// Fires for ALL status changes: sync completion, webhook delivery, etc.
},
},
});
// Input handler pattern for clarifications
const handler = async context => {
return context.inputRequest.field === 'budget' ? 50000 : context.deferToHuman();
};🚨 NEVER manually edit package.json version - Use changesets:
npm run changeset # Create changeset for changes
# Merge PR to main → Auto Release PR created
# Merge Release PR → Auto publish to npmThe correct separation:
package.jsonversion = Library version (managed by changesets)src/lib/version.tsADCP_VERSION = AdCP schema version (can differ from library version)
Library types auto-generated from AdCP schemas:
npm run sync-schemas # Download from protocol repo
npm run generate-types # Generate TypeScript types
npm run generate-zod-schemas # Generate runtime validationThe SDK speaks one AdCP major (ADCP_VERSION in src/lib/version.ts, currently 3.0.1) on its public surface and adapts to legacy sellers underneath. v2.5 is the only active legacy shim; the recipe for adding a new one (e.g. v3 when the SDK pin moves to v4) lives in docs/development/WIRE-VERSION-COMPAT.md. Touch that doc whenever you add to src/lib/adapters/legacy/, src/lib/types/v*-*/, or schemas/cache/<version>/.
Test TaskExecutor patterns by mocking ProtocolClient.callTool():
ProtocolClient.callTool = mock.fn(async (agent, taskName, params) => {
if (taskName === 'tasks/get') return { task: { status: 'working' } };
return { status: 'submitted' };
});npm test # Unit tests
npm run test:protocols # Protocol compliance tests
npm run test:e2e # End-to-end against live server
npm run test:all # Full test suiteTests run under --test-timeout=60000. If a test hangs indefinitely (high CPU, no output) rather than timing out, send SIGQUIT to dump the V8 stack before killing — see CONTRIBUTING.md § Debugging a hung test.
docs/llms.txt- Start here: protocol overview, all tools, types, flows (generated)docs/TYPE-SUMMARY.md- Curated type reference (generated)src/lib/core/- Main client classes (AdCPClient, TaskExecutor)src/lib/protocols/- A2A/MCP protocol implementationssrc/lib/types/- Generated TypeScript types from schemas (prefer TYPE-SUMMARY.md)compliance/cache/- Storyboards pulled fromadcontextprotocol.org/compliance/{version}/(gitignored; populated bynpm run sync-schemas)test/lib/- Library unit teststest/e2e/- Integration testsexamples/- Usage examples and demos
This is a published npm library. Callers on older versions must not break when we add new required fields.
When adding a new required field to a request schema:
- Infer it from existing fields in
SingleAgentClient.normalizeRequestParams()so callers that don't send it still work - Update all internal callers (testing scenarios) to send the field explicitly
- Add tests verifying the inference works and that explicit values are preserved
Example: buying_mode was added as required on get_products. The client infers it from brief presence — callers that only sent { brief: '...' } keep working.
The pattern:
normalizeRequestParams()runs before validation, filling in derivable fieldsvalidateRequest()runs Zod schemas after normalizationadaptRequestForServerVersion()handles v3→v2 downgrades for older servers
Never add a required field without a backwards-compatible default or inference path.
- Protocol clients: Always use official
@a2a-js/sdkand@modelcontextprotocol/sdk - Mock data: Never inject fallback data when agents return empty responses
- Version management: Let changesets handle package.json, edit ADCP_VERSION separately
- Backwards compatibility: New required schema fields need inference in
normalizeRequestParams()