Skip to content

Commit e0eef4c

Browse files
authored
Merge pull request #169 from adcontextprotocol/bokelley/python-server-dx
feat: skill-based agent generation with storyboard validation
2 parents ba2d9c7 + d9d8778 commit e0eef4c

File tree

13 files changed

+2853
-14
lines changed

13 files changed

+2853
-14
lines changed

README.md

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,49 @@
44
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
55
[![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
66

7-
Official Python client for the **Ad Context Protocol (AdCP)**. Build distributed advertising operations that work synchronously OR asynchronously with the same code.
7+
Official Python SDK for the **Ad Context Protocol (AdCP)**. Build and connect to advertising agents that work synchronously OR asynchronously with the same code.
8+
9+
## Building an AdCP Agent
10+
11+
The fastest path to a working agent: subclass `ADCPHandler`, use response builders, call `serve()`.
12+
13+
```python
14+
from adcp.server import ADCPHandler, serve
15+
from adcp.server.responses import capabilities_response, products_response
16+
17+
class MySeller(ADCPHandler):
18+
async def get_adcp_capabilities(self, params, context=None):
19+
return capabilities_response(["media_buy"])
20+
21+
async def get_products(self, params, context=None):
22+
return products_response(MY_PRODUCTS)
23+
24+
# implement create_media_buy, get_media_buys, sync_creatives, etc.
25+
26+
serve(MySeller(), name="my-seller")
27+
```
28+
29+
Validate with storyboards:
30+
```bash
31+
python agent.py &
32+
npx @adcp/client storyboard run http://localhost:3001/mcp media_buy_seller --json
33+
```
34+
35+
| Agent type | Skill | Storyboard | Steps |
36+
|-----------|-------|-----------|-------|
37+
| Seller (publisher, SSP, retail media) | [`skills/build-seller-agent/`](skills/build-seller-agent/SKILL.md) | `media_buy_seller` | 9 |
38+
| Signals (audience data, CDP) | [`skills/build-signals-agent/`](skills/build-signals-agent/SKILL.md) | `signal_owned` | 4 |
39+
| Creative (ad server, CMP) | [`skills/build-creative-agent/`](skills/build-creative-agent/SKILL.md) | `creative_lifecycle` | 6 |
40+
41+
For compliance testing, add a `TestControllerStore` so storyboards can force state transitions:
42+
```python
43+
from adcp.server.test_controller import TestControllerStore
44+
serve(MySeller(), name="my-seller", test_controller=MyStore())
45+
```
46+
47+
Each skill file in [`skills/`](skills/) contains the complete pattern, response shapes, and validation loop for coding agents (Claude, Codex) to generate passing servers.
48+
49+
## Connecting to AdCP Agents
850

951
## The Core Concept
1052

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
---
2+
name: build-creative-agent
3+
description: Use when building an AdCP creative agent — an ad server, creative management platform, or any system that accepts, stores, transforms, and serves ad creatives.
4+
---
5+
6+
# Build a Creative Agent (Python)
7+
8+
## Overview
9+
10+
A creative agent manages the creative lifecycle: accepts assets from buyers, stores them in a library, builds serving tags, and renders previews. Unlike a generative seller (which also sells inventory), a creative agent is a standalone creative platform.
11+
12+
## When to Use
13+
14+
- User wants to build an ad server, creative management platform, or creative rendering service
15+
- User mentions `build_creative`, `preview_creative`, `sync_creatives`, or `list_creatives`
16+
- User references creative formats, VAST tags, serving tags, or creative libraries
17+
18+
**Not this skill:**
19+
- Selling inventory + generating creatives → `skills/build-generative-seller-agent/`
20+
- Selling inventory (no creative management) → `skills/build-seller-agent/`
21+
- Serving audience segments → `skills/build-signals-agent/`
22+
23+
## Before Writing Code
24+
25+
Determine these things. Ask the user — don't guess.
26+
27+
### 1. What kind of creative platform?
28+
29+
- **Ad server** (Innovid, Flashtalking) — stateful library, builds serving tags (VAST, display tags)
30+
- **Creative management platform** (Celtra) — format transformation, template rendering
31+
- **Publisher creative service** — accepts buyer assets, validates against publisher specs
32+
33+
### 2. What formats?
34+
35+
Get specific formats. Each format needs: dimensions, accepted asset types, mime types.
36+
- **Display**: `display_300x250`, `display_728x90`
37+
- **Video**: `video_30s`, `vast_30s`
38+
- **Native**: `native_content` (image + headline + description)
39+
40+
### 3. What operations?
41+
42+
- **Sync** — accept and store creatives (always needed)
43+
- **List** — query the library with filtering (needed for storyboard)
44+
- **Preview** — render a visual preview (needed for storyboard)
45+
- **Build** — produce serving tags from stored creatives (needed for storyboard)
46+
47+
## Architecture
48+
49+
One file. Subclass `ADCPHandler`, override the tools you support, call `serve()`. Use an in-memory dict to store synced creatives.
50+
51+
```python
52+
from adcp.server import ADCPHandler, serve
53+
from adcp.server.responses import (
54+
capabilities_response, creative_formats_response, sync_creatives_response,
55+
list_creatives_response, preview_creative_response, build_creative_response,
56+
)
57+
58+
creatives: dict[str, dict] = {} # in-memory creative library
59+
60+
class MyCreativeAgent(ADCPHandler):
61+
async def get_adcp_capabilities(self, params, context=None):
62+
return capabilities_response(["creative"])
63+
# ... implement tools
64+
65+
serve(MyCreativeAgent(), name="my-creative-agent")
66+
```
67+
68+
## Tools and Required Response Shapes
69+
70+
Every tool uses a response builder from `adcp.server.responses`.
71+
72+
**`get_adcp_capabilities`**
73+
```python
74+
from adcp.server.responses import capabilities_response
75+
76+
async def get_adcp_capabilities(self, params, context=None):
77+
return capabilities_response(["creative"])
78+
```
79+
80+
**`list_creative_formats`**
81+
```python
82+
from adcp.server.responses import creative_formats_response
83+
84+
AGENT_URL = "http://localhost:3001/mcp"
85+
86+
async def list_creative_formats(self, params, context=None):
87+
return creative_formats_response([
88+
{
89+
"format_id": {"agent_url": AGENT_URL, "id": "display_300x250"},
90+
"name": "Display 300x250",
91+
"description": "Standard IAB medium rectangle",
92+
"renders": [{"width": 300, "height": 250}],
93+
"assets": [{
94+
"item_type": "individual",
95+
"asset_id": "image",
96+
"asset_type": "image",
97+
"required": True,
98+
"accepted_media_types": ["image/png", "image/jpeg"],
99+
}],
100+
},
101+
{
102+
"format_id": {"agent_url": AGENT_URL, "id": "video_30s"},
103+
"name": "Video 30s Pre-Roll",
104+
"renders": [{"width": 1920, "height": 1080}],
105+
"assets": [{
106+
"item_type": "individual",
107+
"asset_id": "video",
108+
"asset_type": "video",
109+
"required": True,
110+
"accepted_media_types": ["video/mp4"],
111+
}],
112+
},
113+
])
114+
```
115+
116+
**`sync_creatives`** — store creatives in the library. Status must be a valid `CreativeStatus`: `processing`, `pending_review`, `approved`, `rejected`, `archived`.
117+
```python
118+
from adcp.server.responses import sync_creatives_response
119+
120+
async def sync_creatives(self, params, context=None):
121+
results = []
122+
for c in params.get("creatives", []):
123+
creative_id = c.get("creative_id", f"c-{uuid.uuid4().hex[:8]}")
124+
creatives[creative_id] = {**c, "creative_id": creative_id, "status": "approved"}
125+
results.append({
126+
"creative_id": creative_id,
127+
"action": "created",
128+
"status": "approved",
129+
})
130+
return sync_creatives_response(results)
131+
```
132+
133+
**`list_creatives`** — query the library. Must include `pagination` and `query_summary` fields. Status must be a valid `CreativeStatus`.
134+
```python
135+
from adcp.server.responses import list_creatives_response
136+
137+
async def list_creatives(self, params, context=None):
138+
results = list(creatives.values())
139+
140+
# Filter by format_ids if provided
141+
filters = params.get("filters") or {}
142+
if format_ids := filters.get("format_ids"):
143+
format_id_set = {f.get("id", "") if isinstance(f, dict) else str(f) for f in format_ids}
144+
results = [c for c in results if c.get("format_id", {}).get("id") in format_id_set]
145+
146+
serialized = [
147+
{
148+
"creative_id": c["creative_id"],
149+
"name": c.get("name", ""),
150+
"format_id": c.get("format_id"),
151+
"status": c.get("status", "approved"),
152+
"created_date": "2026-01-01T00:00:00Z",
153+
"updated_date": "2026-01-01T00:00:00Z",
154+
}
155+
for c in results
156+
]
157+
return list_creatives_response(serialized)
158+
```
159+
160+
**`preview_creative`** — render a preview of a stored creative
161+
```python
162+
from adcp.server.responses import preview_creative_response
163+
164+
async def preview_creative(self, params, context=None):
165+
creative_id = params.get("creative_id")
166+
creative = creatives.get(creative_id) if creative_id else None
167+
format_id = params.get("format_id") or (creative or {}).get("format_id", {})
168+
169+
return preview_creative_response([{
170+
"preview_id": f"prev-{uuid.uuid4().hex[:8]}",
171+
"input": {
172+
"format_id": format_id,
173+
"name": (creative or {}).get("name", "Preview"),
174+
"assets": (creative or {}).get("assets", {}),
175+
},
176+
"renders": [{
177+
"render_id": f"render-{uuid.uuid4().hex[:8]}",
178+
"output_format": "url",
179+
"preview_url": f"https://example.com/preview/{creative_id or 'unknown'}.png",
180+
"role": "primary",
181+
"dimensions": {"width": 300, "height": 250},
182+
}],
183+
}])
184+
```
185+
186+
**`build_creative`** — produce serving tags. The storyboard sends `target_format_id` (not `output_format`). Look up the creative by ID, by format match, or fall back to the first available.
187+
```python
188+
from adcp.server.responses import build_creative_response
189+
190+
async def build_creative(self, params, context=None):
191+
creative_id = params.get("creative_id")
192+
creative = creatives.get(creative_id) if creative_id else None
193+
194+
# Resolve target format
195+
target_format = params.get("target_format_id") or params.get("output_format") or params.get("format_id")
196+
197+
# Find creative by format if not found by ID
198+
if not creative and target_format:
199+
target_id = target_format.get("id", "") if isinstance(target_format, dict) else str(target_format)
200+
for c in creatives.values():
201+
if c.get("format_id", {}).get("id") == target_id:
202+
creative = c
203+
break
204+
205+
# Fall back to first available
206+
if not creative and creatives:
207+
creative = next(iter(creatives.values()))
208+
209+
format_id = target_format or (creative or {}).get("format_id", {})
210+
211+
# Build assets dict from stored creative
212+
stored_assets = (creative or {}).get("assets", [])
213+
built_assets = {}
214+
if isinstance(stored_assets, list):
215+
for asset in stored_assets:
216+
built_assets[asset.get("asset_id", "unknown")] = asset
217+
elif isinstance(stored_assets, dict):
218+
built_assets = stored_assets
219+
220+
return build_creative_response({
221+
"format_id": format_id,
222+
"name": (creative or {}).get("name", "Built Creative"),
223+
"assets": built_assets,
224+
})
225+
```
226+
227+
## SDK Quick Reference
228+
229+
| Function | Usage |
230+
|----------|-------|
231+
| `serve(handler)` | Start server on `:3001/mcp` |
232+
| `capabilities_response(protocols)` | `get_adcp_capabilities` response |
233+
| `creative_formats_response(formats)` | `list_creative_formats` response |
234+
| `sync_creatives_response(creatives)` | `sync_creatives` response |
235+
| `list_creatives_response(creatives)` | `list_creatives` response (adds pagination, query_summary) |
236+
| `preview_creative_response(previews)` | `preview_creative` response (adds response_type, expires_at) |
237+
| `build_creative_response(manifest)` | `build_creative` response |
238+
239+
Import handlers from `adcp.server`. Import response builders from `adcp.server.responses`.
240+
241+
## Validation
242+
243+
```bash
244+
python agent.py &
245+
npx @adcp/client storyboard run http://localhost:3001/mcp creative_lifecycle --json
246+
```
247+
248+
**Keep iterating until all steps pass.**
249+
250+
## Common Mistakes
251+
252+
| Mistake | Fix |
253+
|---------|-----|
254+
| Skip `get_adcp_capabilities` | Must be implemented |
255+
| Return raw dicts without builders | Use response builders for every tool |
256+
| Wrong creative status | Must be `approved`, not `accepted`. Valid: `processing`, `pending_review`, `approved`, `rejected`, `archived` |
257+
| `list_creatives` ignores format filter | Check `filters.format_ids` and filter results |
258+
| `list_creatives` missing pagination/query_summary | Use `list_creatives_response()` which adds them automatically |
259+
| `build_creative` can't find creative | Check `target_format_id` param (not `output_format`), fall back to first available |
260+
| No in-memory store for synced creatives | `list_creatives`, `preview_creative`, `build_creative` need previously synced creatives |
261+
262+
## Reference
263+
264+
This skill contains everything needed to build a 6/6 passing creative agent. The code blocks above are taken from a validated implementation.

0 commit comments

Comments
 (0)