Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion skills/build-creative-agent/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ async def build_creative(self, params, context=None):

| Function | Usage |
|----------|-------|
| `serve(handler)` | Start server on `:3001/mcp` |
| `serve(handler, transport="a2a"\|"streamable-http", port=3001)` | Start MCP or A2A server. Context passthrough is automatic. |
| `capabilities_response(protocols)` | `get_adcp_capabilities` response |
| `creative_formats_response(formats)` | `list_creative_formats` response |
| `sync_creatives_response(creatives)` | `sync_creatives` response |
Expand Down
39 changes: 28 additions & 11 deletions skills/build-seller-agent/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Pricing models:
One file. Subclass `ADCPHandler`, override the tools you support, call `serve()`.

```python
from adcp.server import ADCPHandler, serve, adcp_error, resolve_account, inject_context
from adcp.server import ADCPHandler, serve, adcp_error, resolve_account
from adcp.server.responses import capabilities_response, products_response, media_buy_response
from adcp.server.test_controller import TestControllerStore

Expand All @@ -76,6 +76,8 @@ serve(MySeller(), name="my-seller", test_controller=MyStore())

## Product Construction Example

Every product needs `description`, `reporting_capabilities`, and `delivery_measurement` — these are required by the schema and the storyboard validator.

```python
PRODUCTS = [
{
Expand All @@ -87,7 +89,7 @@ PRODUCTS = [
{"publisher_domain": "example.com", "selection_type": "all"}
],
"format_ids": [
{"agent_url": "http://localhost:3001/mcp", "id": "display_970x250"}
{"agent_url": AGENT_URL, "id": "display_970x250"}
],
"pricing_options": [
{
Expand All @@ -97,6 +99,19 @@ PRODUCTS = [
"currency": "USD",
}
],
"reporting_capabilities": {
"available_metrics": ["impressions", "spend", "clicks", "ctr"],
"available_reporting_frequencies": ["hourly", "daily"], # hourly|daily|monthly only
"date_range_support": "date_range", # or "lifetime_only"
"supports_webhooks": False,
"expected_delay_minutes": 60,
"timezone": "UTC",
},
"delivery_measurement": {
"measurement_type": "server_side",
"verification": "internal",
"provider": "internal",
},
},
]
```
Expand All @@ -105,13 +120,15 @@ PRODUCTS = [

The SDK provides helpers that eliminate common boilerplate. Import from `adcp.server`:

**Automatic behaviors** (no handler code needed):
- **Context passthrough** — if the request has a `context` field, it's echoed back in the response automatically.

| Helper | What it does |
|--------|-------------|
| `adcp_error(code, message, field=, suggestion=)` | Structured error with auto-recovery classification (20+ standard codes) |
| `media_buy_response(..., status="active")` | Auto-populates `valid_actions` from status, auto-sets `revision` and `confirmed_at` |
| `cancel_media_buy_response(id, "buyer")` | Auto-sets `canceled_at`, `status`, `valid_actions=[]` |
| `resolve_account(params, resolver)` | Auto-resolves AccountReference, returns ACCOUNT_NOT_FOUND if missing |
| `inject_context(params, response)` | Echoes `context` field from request to response (ADCP requirement) |
| `valid_actions_for_status(status)` | Maps status to valid buyer actions |
| `is_terminal_status(status)` | True for completed/rejected/canceled |
| `AccountError(code, message, suggestion=)` | Raise from resolver for suspended/payment/ambiguous accounts |
Expand Down Expand Up @@ -257,7 +274,7 @@ async def get_products(self, params, context=None):

**`create_media_buy`**
```python
from adcp.server import adcp_error, inject_context
from adcp.server import adcp_error
from adcp.server.responses import media_buy_response

async def create_media_buy(self, params, context=None):
Expand All @@ -283,8 +300,7 @@ async def create_media_buy(self, params, context=None):
# Store so get_media_buys and test controller can find it
media_buys[mb_id] = {"status": "active", "currency": "USD", "packages": packages}
# status="active" auto-populates valid_actions, revision, confirmed_at
resp = media_buy_response(mb_id, packages, status="active")
return inject_context(params, resp)
return media_buy_response(mb_id, packages, status="active")
```

**`get_media_buys`**
Expand All @@ -308,7 +324,7 @@ async def get_media_buys(self, params, context=None):

**`update_media_buy`** — handles pause, resume, cancel, budget changes, package updates.
```python
from adcp.server import adcp_error, inject_context, cancel_media_buy_response
from adcp.server import adcp_error, cancel_media_buy_response
from adcp.server.responses import update_media_buy_response

async def update_media_buy(self, params, context=None):
Expand Down Expand Up @@ -342,10 +358,9 @@ async def update_media_buy(self, params, context=None):
# Apply budget, dates, etc.

mb["revision"] = mb.get("revision", 1) + 1
resp = update_media_buy_response(
return update_media_buy_response(
mb_id, status=mb["status"], revision=mb["revision"]
)
return inject_context(params, resp)
```

**`list_creative_formats`**
Expand Down Expand Up @@ -490,9 +505,8 @@ return capabilities_response(["media_buy", "compliance_testing"])
| `adcp_error(code, message, field=, suggestion=)` | Structured error with auto-recovery |
| `cancel_media_buy_response(id, "buyer"/"seller")` | Cancellation with auto-defaults |
| `resolve_account(params, resolver)` | Account resolution with auto-error |
| `inject_context(params, response)` | Context passthrough (ADCP requirement) |
| `valid_actions_for_status(status)` | Status-to-actions mapping |
| `serve(handler, test_controller=store)` | Start server on `:3001/mcp` |
| `serve(handler, transport="a2a"\|"streamable-http", port=3001, test_controller=store)` | Start MCP or A2A server. Context passthrough is automatic. |

Import helpers from `adcp.server`. Import response builders from `adcp.server.responses`. Import types from `adcp.types`.

Expand Down Expand Up @@ -525,6 +539,9 @@ npx @adcp/client storyboard run http://localhost:3001/mcp media_buy_seller --jso
| Missing `brand`/`operator` in sync_accounts | Echo them back from the request |
| Not storing entities in memory | Test controller needs to find accounts, media buys, creatives |
| Wrong `delivery_response` signature | Takes `delivery_response(deliveries_list, reporting_period=...)`, not individual metrics |
| Missing `reporting_capabilities` on products | Required. Sub-fields: `available_metrics`, `available_reporting_frequencies`, `date_range_support`, `supports_webhooks` |
| `weekly` in `available_reporting_frequencies` | Only `hourly`, `daily`, `monthly` are valid |
| Missing `delivery_measurement.provider` | Required field — use `"internal"` or third-party provider name |

## Reference

Expand Down
5 changes: 2 additions & 3 deletions skills/build-signals-agent/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ At least one pricing option per signal:
One file. Subclass `ADCPHandler`, override the tools you support, call `serve()`.

```python
from adcp.server import ADCPHandler, serve, adcp_error, inject_context
from adcp.server import ADCPHandler, serve, adcp_error
from adcp.server.responses import capabilities_response, signals_response, activate_signal_response

class MySignalsAgent(ADCPHandler):
Expand Down Expand Up @@ -205,8 +205,7 @@ async def activate_signal(self, params, context=None):
| Function | Usage |
|----------|-------|
| `adcp_error(code, message, field=, suggestion=)` | Structured error with auto-recovery |
| `inject_context(params, response)` | Context passthrough (ADCP requirement) |
| `serve(handler)` | Start server on `:3001/mcp` |
| `serve(handler, transport="a2a"\|"streamable-http", port=3001)` | Start MCP or A2A server. Context passthrough is automatic — no need to call `inject_context` in handlers. |

Import helpers from `adcp.server`. Import response builders from `adcp.server.responses`.

Expand Down
4 changes: 4 additions & 0 deletions src/adcp/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ async def get_products(params, context=None):
from __future__ import annotations

from adcp.capabilities import validate_capabilities
from adcp.server.a2a_server import ADCPAgentExecutor, create_a2a_server
from adcp.server.base import (
ADCPHandler,
NotImplementedResponse,
Expand Down Expand Up @@ -134,6 +135,9 @@ async def get_products(params, context=None):
"create_mcp_server",
"get_tools_for_handler",
"serve",
# A2A integration
"ADCPAgentExecutor",
"create_a2a_server",
# Test controller
"TestControllerStore",
"TestControllerError",
Expand Down
Loading
Loading