Context
AdCP 3.0 defines the transport-neutral envelope (`protocol-envelope.json`), request/response schemas per tool, and the `task_id` / `status: 'submitted'` pattern for async work. It does not define how an AdCP agent is exposed over A2A JSON-RPC specifically — only over MCP.
As A2A-native buyer agents emerge (Google's agent-to-agent tooling, Scope3 harnesses, others), AdCP sellers implementing both transports will diverge on three decisions that the spec doesn't pin:
- How does a Message address a skill? A2A's `Message` carries free-form `parts[]`. AdCP tools need a routing discriminant. There is no de-facto convention across implementations.
- Where does `task_id` live when an AdCP handler returns a Submitted arm over A2A? The AdCP payload has a `task_id` field. A2A also has its own `Task.id` generated by the server. Are they the same? Does the AdCP handle ride inside the DataPart `data`, inside `artifact.metadata`, or somewhere else?
- What `Task.state` does the adapter emit when the AdCP work is async-pending? A2A 0.3.0 treats `submitted` as the INITIAL lifecycle state before `working`, not a terminal state. Emitting `state: 'submitted'` with `final: true` on a synchronous `message/send` that acknowledges a queued AdCP task is non-conformant per A2A's lifecycle, but it's the obvious-looking mapping.
`@adcp/client` just shipped an A2A adapter (adcontextprotocol/adcp-client#899) that made choices on all three. The choices matter: same-SDK client/server mismatch on (1) would have been a silent routing failure; wrong-choice on (3) would confuse Google's A2A tooling. Reviewers on that PR flagged that without a spec binding, other implementations will choose differently.
Proposal
Publish a normative AdCP-over-A2A binding in the spec alongside the MCP path. Proposed rules, matching what `@adcp/client`'s adapter ships:
Skill addressing
A client invokes an AdCP tool by sending a `Message` with exactly one `DataPart`:
```json
{ "kind": "data", "data": { "skill": "<tool_name>", "input": { ... } } }
```
- `skill` (required, string) matches an AdCP tool name (`get_products`, `create_media_buy`, etc.).
- `input` (required, object) carries the tool's typed AdCP request payload as defined by the existing per-tool request schemas.
- Multi-part Messages, text-only Messages, and Messages missing the `skill` field are routing errors (A2A JSON-RPC `-32602` or AdCP `VALIDATION_ERROR` inside a failed Task, per implementation).
Response mapping
Per handler-return → A2A surface:
| AdCP return |
A2A `Task.state` |
Artifact |
| Success arm |
`completed` |
`DataPart` with the typed AdCP response |
| Submitted arm (`status: 'submitted'`) |
`completed` |
`DataPart` with the AdCP response; `artifact.metadata.adcp_task_id` = the AdCP-level `task_id` |
| Error arm (`errors[]`) |
`failed` |
`DataPart` with the Error arm payload |
| Framework error (`adcp_error`) |
`failed` |
`DataPart` with `adcp_error` |
Rationale for Submitted → `completed`
A2A `Task.state` tracks the transport call lifecycle (did the JSON-RPC request complete?). AdCP `status` inside the artifact tracks the work (submitted / completed / failed). A Submitted AdCP arm on a synchronous `message/send` means the transport call finished; the ad-tech work is queued. Buyers resume the AdCP work via `artifact.metadata.adcp_task_id`. This keeps the A2A Task lifecycle clean and lets the two state machines compose without conflict.
Rationale for `artifact.metadata` (not `data`)
`artifact.data` should validate against the AdCP tool's response schema. Injecting transport-level metadata (`adcp_task_id`) into `data` pollutes that surface. `Artifact.metadata` is A2A 0.3.0's spec-sanctioned extension channel. Keeping the two clean means storyboards can `model_validate` the artifact payload against AdCP schemas without special-casing transport fields.
Adoption path
- Draft the binding as a new section under `schemas/cache//protocols/` (or as a sibling doc).
- Reference it from each protocol's existing bindings doc (currently MCP-implicit).
- Call out the `Task.state` lifecycle rationale explicitly so adapter authors don't re-derive it.
References
Happy to iterate on the exact mapping language; the three decisions above are the axes that need pinning.
Context
AdCP 3.0 defines the transport-neutral envelope (`protocol-envelope.json`), request/response schemas per tool, and the `task_id` / `status: 'submitted'` pattern for async work. It does not define how an AdCP agent is exposed over A2A JSON-RPC specifically — only over MCP.
As A2A-native buyer agents emerge (Google's agent-to-agent tooling, Scope3 harnesses, others), AdCP sellers implementing both transports will diverge on three decisions that the spec doesn't pin:
`@adcp/client` just shipped an A2A adapter (adcontextprotocol/adcp-client#899) that made choices on all three. The choices matter: same-SDK client/server mismatch on (1) would have been a silent routing failure; wrong-choice on (3) would confuse Google's A2A tooling. Reviewers on that PR flagged that without a spec binding, other implementations will choose differently.
Proposal
Publish a normative AdCP-over-A2A binding in the spec alongside the MCP path. Proposed rules, matching what `@adcp/client`'s adapter ships:
Skill addressing
A client invokes an AdCP tool by sending a `Message` with exactly one `DataPart`:
```json
{ "kind": "data", "data": { "skill": "<tool_name>", "input": { ... } } }
```
Response mapping
Per handler-return → A2A surface:
Rationale for Submitted → `completed`
A2A `Task.state` tracks the transport call lifecycle (did the JSON-RPC request complete?). AdCP `status` inside the artifact tracks the work (submitted / completed / failed). A Submitted AdCP arm on a synchronous `message/send` means the transport call finished; the ad-tech work is queued. Buyers resume the AdCP work via `artifact.metadata.adcp_task_id`. This keeps the A2A Task lifecycle clean and lets the two state machines compose without conflict.
Rationale for `artifact.metadata` (not `data`)
`artifact.data` should validate against the AdCP tool's response schema. Injecting transport-level metadata (`adcp_task_id`) into `data` pollutes that surface. `Artifact.metadata` is A2A 0.3.0's spec-sanctioned extension channel. Keeping the two clean means storyboards can `model_validate` the artifact payload against AdCP schemas without special-casing transport fields.
Adoption path
References
Happy to iterate on the exact mapping language; the three decisions above are the axes that need pinning.