feat: ADCP 3.0 server DX helpers, type guards, and schema sync#174
Conversation
Major DX improvements for building ADCP agents: - Server helpers: adcp_error() with 32 spec error codes and auto-recovery classification, valid_actions_for_status() state machine, resolve_account() with AccountError for suspended/payment/ambiguous, inject_context() with size limits, cancel_media_buy_response() with auto-defaults - Decorator server builder: adcp_server() with auto-capabilities detection, typo detection (ValueError on unknown tasks) - Type guards: is_adcp_success/is_adcp_error plus 15 typed guards for response discrimination - Typed handler params: all 52 ADCPHandler methods accept RequestType | dict - Collection lists: 5 CRUD tasks wired into handler, MCP tools, and types - Capabilities: TASK_FEATURE_MAP expanded to 48 tasks across 12 domains, build_synthetic_capabilities(), supports_v3() - Error handling: ADCPTaskError with is_retryable, ADCPSimpleAPIError preserves structured errors, plain text suggestions (no emoji) - Schema sync: ADCP_VERSION updated to latest, 434 schemas, collection and async response schemas downloaded - Documentation: llms.txt, AGENTS.md, updated skills with DX helpers, proposal workflow, update_media_buy, account resolution patterns - Response builders: media_buy_response auto-populates valid_actions, revision, confirmed_at from status Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add noqa: F401 to type guard re-exports in types/__init__.py (these are intentional re-exports, not unused imports) - Make model_rebuild() calls in consolidate_exports.py conditional on whether the type exists (upstream schema changes may remove PreviewCreativeRequest1/2) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add noqa: F401 to re-export imports in adcp/__init__.py (capabilities, exceptions, type guards) - Fix import sorting (I001) in adcp/__init__.py - Pin ADCP_VERSION back to 3.0.0-rc.3 so CI schema check skips (latest is a moving target that breaks aliases.py type names) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
bokelley
left a comment
There was a problem hiding this comment.
Review from the Prebid Sales Agent team
We're the largest AdCP server implementation in Python — 16 AdCP tools across MCP + A2A transports, multi-tenant with adapter abstraction (GAM, mock, Xandr). This PR directly affects our adoption path. Sharing feedback both as consumers and as a reference for what real server DX looks like.
Overall: this is a great direction. The helpers, type guards, and builder are all things we've had to build ourselves. Excited to see them in the SDK. Below is concrete feedback on what would make adoption smoother.
1. Promote generated_poc types to stable API (blocking for us)
We have 60+ imports from adcp.types.generated_poc.* — ContextObject, CreativeAsset, MediaBuyStatus, PaginationRequest, PaginationResponse, AccountReference, TargetingOverlay, BrandReference, etc. These are not edge cases; they're the core types any real server needs.
The 434-schema sync in this PR could break all of them silently. We need these importable from adcp.types without reaching into generated internals. Without stable paths, every schema sync is a potential breaking change for consumers.
Ask: promote commonly-used types to adcp.types.* before this ships. At minimum: ContextObject, CreativeAsset, MediaBuyStatus, PaginationRequest, PaginationResponse, TargetingOverlay, BrandReference, AccountReference, CreativeAction, FormatCategory, AssetContentType.
2. Error handling: our approach is stronger for servers — can the SDK meet us halfway?
We built a typed exception hierarchy (src/core/exceptions.py) with 16 exception classes, each carrying error_code, status_code, and recovery (transient/correctable/terminal). This is better for servers than adcp_error() returning dicts because:
- Exceptions flow naturally through Python's transport boundaries
- Structural guards enforce that
_implfunctions raiseAdCPError, neverToolError - Type checkers catch misuse at definition time
- Transport layers (MCP, A2A) each have a single translator that maps the hierarchy to their protocol's error format
What we'd adopt from this PR: the 32 standard error codes. We currently cover 14/32 — missing codes like STATUS_TRANSITION_INVALID, REVISION_CONFLICT, CREATIVE_REJECTED, CREATIVE_POLICY_VIOLATION, IDEMPOTENCY_CONFLICT, UPSTREAM_TIMEOUT, etc.
Ask: publish the 32 standard error codes as a Literal union or Enum, not just a dict in helpers.py. Something like:
from adcp.types import AdCPErrorCode # Literal["VALIDATION_ERROR", "AUTH_TOKEN_INVALID", ...]
class AdCPError(Exception):
error_code: AdCPErrorCode # type-checked at definition timeThis lets servers validate their error codes against the spec at import time. The dict-of-dicts in STANDARD_ERROR_CODES is useful for adcp_error() but not for servers that build their own exception hierarchy.
Also: the recovery classification (transient/correctable/terminal) auto-derived from error codes is smart. Consider exporting TRANSIENT_CODES, CORRECTABLE_CODES, TERMINAL_CODES as sets so servers can stay aligned without duplicating the classification logic.
3. valid_actions_for_status() — we need this, but as typed data
We have zero valid_actions logic today — this is a gap. We'd adopt this immediately. But:
- What's the return type?
list[str]? If so, what's the canonical action vocabulary? These should be aLiteralunion or enum inadcp.types, not just strings. - The state machine mapping (status → valid actions) is spec-level data. It should be importable as a constant dict, not only accessible through a helper function. Servers need to inspect it, test against it, and potentially extend it.
Ask: export MEDIA_BUY_STATE_MACHINE: dict[MediaBuyStatus, list[MediaBuyAction]] as a public constant alongside the helper function.
4. A2A server DX — the biggest pain point you could solve
Our A2A server (adcp_a2a_server.py, ~2300 lines) has significant boilerplate that the SDK could eliminate:
a) Handler routing boilerplate. Every handler follows the same pattern: validate params via Pydantic model → resolve identity → call _impl → serialize response. We have 16 of these. The builder pattern in this PR is close but oriented toward ADCPHandler subclasses. For servers using the _impl pattern (transport-agnostic business logic + thin wrappers), a decorator that auto-routes would be transformative:
@adcp_handler("create_media_buy", CreateMediaBuyRequest)
async def handle_create(req: CreateMediaBuyRequest, identity: ResolvedIdentity):
return await _create_media_buy_impl(req, identity=identity)b) Error translation duplication. We have the same AdCPError → protocol error translation in both our MCP layer and A2A layer, implemented separately. The SDK could provide translate_error(exc, protocol="a2a"|"mcp") to centralize this.
c) Serialization inconsistency. Our handlers return a mix of Pydantic models and raw dicts (especially for error paths). A consistent contract — "handlers always return BaseModel, framework handles serialization" — would eliminate a class of bugs.
d) Request normalization. We built request_compat.py to handle field renames across AdCP versions (account_id → account: {account_id}, campaign_ref → buyer_campaign_ref, promoted_offerings → catalogs, etc.). This is spec-level knowledge that should live in the SDK, not in every server. Consider a normalize_request(task_name, params, client_version) helper.
5. TASK_FEATURE_MAP — great, needs one thing
The expansion from 2 → 48 tasks is exactly what we need for capabilities reporting. We currently build capabilities dynamically from adapter config. build_synthetic_capabilities() could replace a lot of that.
Ask: make the map bidirectional — given a capability, which tasks require it? We need this for feature-gating: "tenant doesn't have creative management enabled → hide sync_creatives from capabilities."
6. Type guards — easy win, will adopt
is_adcp_success() / is_adcp_error() are useful. The 15 typed guards are even better. No feedback — these are clean.
7. inject_context() 64KB limit
Is this configurable? We pass context objects for various operations and 64KB is probably fine today, but a hard-coded limit with no escape hatch will eventually bite someone. At minimum, document what happens at the boundary (truncate? raise?).
8. Collection lists
New domain, we'll track it. No urgency for us but good to see the coverage expanding.
9. Schema sync changelog
434 schemas changed. What fields were removed/renamed? A BREAKING_CHANGES.md or at least a summary of removed fields per schema would save every consumer hours of debugging at upgrade time. This is especially important given the generated_poc stability question above.
Summary of asks (priority order):
- Stable import paths for commonly-used
generated_poctypes - Error code enum/literal for type-safe server error hierarchies
- Recovery classification sets exported as constants
- Valid actions state machine as public constant + typed action vocabulary
- Request normalization helper for cross-version field renames
- Error translation helper for multi-transport servers
- Schema sync changelog for breaking field changes
- TASK_FEATURE_MAP bidirectional lookup
- inject_context limit configurability or documentation
Happy to pair on any of these — we have a battle-tested server implementation that could serve as a reference for DX design decisions.
- Fix I001 import sorting in types/__init__.py (Collection* imports were out of alphabetical order within the import block) - Pin ADCP_VERSION to 3.0.0-rc.3 (latest is a moving target that breaks aliases.py when upstream schema type names change) - Add noqa: F401 to re-export imports in adcp/__init__.py Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Update params type in governance.py, content_standards.py, and sponsored_intelligence.py to match base class typed signatures (RequestType | dict[str, Any]). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add mypy override for generated_poc to disable valid-type errors (upstream schemas use 'list' as field name which shadows builtin) - Update governance, content_standards, sponsored_intelligence handler params to match base class typed signatures Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add MCP ToolAnnotations (readOnlyHint, destructiveHint, idempotentHint) to all 56 tools following the JS client's annotation map - Rewrite tool descriptions for agent consumption: what it does, when to call it, what it returns, what IDs you need from other tools - Closes the ToolAnnotation gap identified in prebid/salesagent#1183 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…xport, missing types Addresses feedback from prebid/salesagent team review: - Export PaginationResponse from adcp.types (was missing) - Export TRANSIENT_CODES, CORRECTABLE_CODES, TERMINAL_CODES as frozensets for servers building their own error hierarchies - Rename _STATUS_ACTIONS to MEDIA_BUY_STATE_MACHINE (public constant) - Document FEATURE_HANDLER_MAP as bidirectional lookup - Use TRANSIENT_CODES in ADCPTaskError.is_retryable (single source of truth) - Clean up module-level loop variables in capabilities.py Filed follow-up issues: - A2A server support: #175 - Storyboard A2A testing: adcontextprotocol/adcp-client#549 - Error translation helper: #176 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Response to salesagent team reviewThanks for the thorough review. Here's what we've addressed and what's tracked as follow-ups. Fixed in this PR
Regarding stable import paths9/11 of your requested types are already in Filed as follow-up issues
On error code enum vs dictGood point about type-safe error codes. The On A2A server DXThis is the biggest gap. Your A2A server is ~2300 lines of boilerplate the SDK should eliminate. #175 tracks this. Your implementation would be a valuable reference for the design. |
…CHINE, PaginationResponse) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Respond to Prebid Sales Agent team feedback on #178: translate_error(): - Returns actual SDK types: ToolError (MCP) and ServerError (A2A) - A2A errors use InvalidParamsError for correctable, InternalError for transient/terminal — enables buyer agent retry/fix/abandon decisions - Preserves recovery classification, error_code, suggestion, details, and original error list in A2A error data field - Error codes align with #174 standard codes (AUTH_REQUIRED, SERVICE_UNAVAILABLE) instead of ad-hoc codes - ADCPTaskError preserves original error codes from the response normalize_request(): - account_id → account: {account_id: "..."} (structural reshape) - brand_manifest URL → brand: {domain: hostname} (URL parsing) - promoted_offerings → catalogs (global rename) - campaign_ref → buyer_campaign_ref (create_media_buy only, tool-scoped) - Package-level optimization_goal → optimization_goals (scalar→array) - Package-level catalog → catalogs (scalar→array) - All transforms respect existing fields (no overwrites) - Package dicts are shallow-copied to avoid mutating originals 39 tests covering all transforms, both protocols, error context preservation, tool-scoping, and edge cases. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Major DX improvements for building ADCP agents in Python, bringing parity with key JS client patterns.
Server DX Helpers (
adcp.server.helpers)adcp_error()— structured errors with all 32 spec error codes and auto-recovery classification (transient/correctable/terminal)valid_actions_for_status()— media buy state machine mapped to spec'spending_activation,active,pausedstatusesresolve_account()withAccountError— handles not-found, suspended, payment-required, ambiguous accountsinject_context()— context passthrough with size limits (security)cancel_media_buy_response()— auto-sets canceled_at, status, valid_actionsDecorator Server Builder
adcp_server()factory with auto-capabilities detection from registered handlersserve()accepts bothADCPHandlerandADCPServerBuilderType Safety
is_adcp_success/is_adcp_error+ 15 typedTypeGuardfunctionsADCPHandlermethods acceptRequestType | dict[str, Any]valid_actions,revision,confirmed_atfrom statusNew Domains
latest: 434 schemas including async response variantsError Handling
ADCPTaskErrorwithis_retryable(checks error codes for transient classification)ADCPSimpleAPIErrorpreserves structured errorsDocumentation
llms.txt— agent-facing quick referenceAGENTS.md— structured handler/builder/error code tables with DX helper examplesadcp_error(), proposal workflow,update_media_buy, account resolutionSecurity
adcp_error()details parameter typed to prevent raw request param reflectioninject_context()enforces 64KB size limit on context passthroughserve()docstring documents auth requirement for productionTest plan
🤖 Generated with Claude Code