with one click
build-generative-seller-agent
// Use when building an AdCP generative seller in Go — an AI ad network or platform that sells inventory AND generates creatives from briefs.
// Use when building an AdCP generative seller in Go — an AI ad network or platform that sells inventory AND generates creatives from briefs.
Use when building an AdCP retail media network agent in Go — sells on-site placements, supports product catalogs, tracks conversions.
Use when building an AdCP seller agent in Go — a publisher, SSP, or retail media network that sells advertising inventory to buyer agents.
Use when building an AdCP creative agent in Go — an ad server, creative management platform, or rendering service.
Use when building an AdCP signals agent in Go — a CDP, data provider, or audience data server that serves targeting segments to buyers.
Use when building an AdCP collection agent in Go — a governance provider, data company, or agency that manages curated lists of content collections (TV shows, podcasts, publications) for targeting and brand safety.
Use when a Go agent needs to send async notifications (task status changes, list changes, artifact batches, revocations) as AdCP-compliant signed webhooks. Covers RFC 9421 webhook-signing, idempotency_key generation, retry, and push_notification_config decoding via the adcp/webhook package.
| name | build-generative-seller-agent |
| description | Use when building an AdCP generative seller in Go — an AI ad network or platform that sells inventory AND generates creatives from briefs. |
Status: Validated against storyboard runner. If validation fails, check the common mistakes table first, then file an issue.
A generative seller does everything a standard seller does (products, media buys, delivery) plus generates creatives from briefs. It MUST also accept standard IAB formats — the generative capability is additive.
Ask the user — don't guess.
brief asset slot. Standard formats need traditional asset slots (image, video).status: "active") or async (status: "submitted"). Async transitions SHOULD emit signed webhooks to push_notification_config.url — see skills/build-webhook-publisher/. Buyer polling is the legacy fallback only.Prefer adcp.Register for seller agents. It wires the standard tools, fills
required idempotency and capability fields, and handles AdCP 3.0/3.1 capability
negotiation. Use adcp.AddTool only for custom tools or when you need a
hand-wired handler.
adcp.AddTool(server, "tool_name", "Description",
func(ctx context.Context, req *mcp.CallToolRequest, input InputType) (*mcp.CallToolResult, any, error) {
return adcp.Result(data, "summary")
})
get_adcp_capabilitiesWhen using adcp.Register, provide capabilities through adcp.Config instead
of hand-wiring this tool. Register accepts GetAdcpCapabilitiesRequest,
honors adcp_version, legacy adcp_major_version, and the protocols filter,
then emits supported_versions.
adcp.Register(server, adcp.Config{
Sandbox: true,
IdempotencyReplayTTL: 24 * time.Hour,
Capabilities: &adcp.CapabilitiesData{
SupportedProtocols: []string{"media_buy", "creative"},
MediaBuy: &adcp.MediaBuyCapabilities{
SupportedPricingModels: []string{"cpm"},
},
Creative: &adcp.CreativeCapabilities{
HasCreativeLibrary: adcp.Bool(true),
},
ComplianceTesting: &adcp.ComplianceTestingCapabilities{
Scenarios: []string{"force_media_buy_status", "force_creative_status"},
},
},
// Wire the handlers below into Config instead of AddTool when using Register.
})
sync_accountsEcho brand/operator back, assign an account_id.
adcp.AddTool(server, "sync_accounts", "Register accounts",
func(ctx context.Context, req *mcp.CallToolRequest, input adcp.SyncAccountsRequest) (*mcp.CallToolResult, any, error) {
var results []adcp.AccountResult
for i, acct := range input.Accounts {
id := fmt.Sprintf("acct-%s-%d", acct.Brand.Domain, i+1)
results = append(results, adcp.AccountResult{
AccountID: id, Brand: acct.Brand, Operator: acct.Operator,
Action: "created", Status: "active",
})
// store account...
}
return adcp.SyncAccountsResponse(results, true)
})
sync_governanceInput has accounts[] with nested account.brand, account.operator, and governance_agents[]. Response key is accounts.
adcp.AddTool(server, "sync_governance", "Register governance agents",
func(ctx context.Context, req *mcp.CallToolRequest, input adcp.SyncGovernanceRequest) (*mcp.CallToolResult, any, error) {
var results []adcp.GovernanceResult
for _, acct := range input.Accounts {
govAcct := acct.Account
if govAcct == nil {
govAcct = &adcp.GovernanceAccount{Brand: acct.Brand, Operator: acct.Operator}
}
results = append(results, adcp.GovernanceResult{
Account: govAcct, Status: "synced", GovernanceAgents: acct.GovernanceAgents,
})
}
return adcp.GovernanceResponse(results)
})
get_productsProducts MUST include description, publisher_properties, format_ids, and reporting_capabilities:
var products = []adcp.Product{
{
ProductID: "ai-display", Name: "AI-Generated Display",
Description: "AI-generated display ads from creative briefs",
Channels: []string{"display"}, DeliveryType: "non_guaranteed",
PublisherProperties: []adcp.PublisherPropertySelector{
{PublisherDomain: "example.com", SelectionType: "all"},
},
PricingOptions: []adcp.PricingOption{
{PricingOptionID: "ai-display-floor", PricingModel: "cpm", FloorPrice: 8.00, Currency: "USD"},
},
FormatIDs: []adcp.FormatRef{
{AgentURL: agentURL, ID: "display_300x250_generative"},
{AgentURL: agentURL, ID: "display_300x250"},
},
ReportingCapabilities: adcp.ReportingCapabilities{
AvailableMetrics: []string{"impressions", "spend", "clicks"},
AvailableReportingFrequencies: []string{"daily"},
ExpectedDelayMinutes: 60,
Timezone: "UTC",
SupportsWebhooks: false,
DateRangeSupport: "date_range",
},
},
}
adcp.AddTool(server, "get_products", "Available products",
func(ctx context.Context, req *mcp.CallToolRequest, input adcp.GetProductsRequest) (*mcp.CallToolResult, any, error) {
return adcp.ProductsResponse(&adcp.ProductsData{Products: products, Sandbox: true})
})
create_media_buyadcp.AddTool(server, "create_media_buy", "Create media buy",
func(ctx context.Context, req *mcp.CallToolRequest, input adcp.CreateMediaBuyRequest) (*mcp.CallToolResult, any, error) {
id := fmt.Sprintf("mb-%d", counter)
var pkgs []adcp.Package
for i, p := range input.Packages {
pkgs = append(pkgs, adcp.Package{
PackageID: fmt.Sprintf("%s-pkg-%d", id, i+1),
ProductID: p.ProductID, PricingOptionID: p.PricingOptionID, Budget: p.Budget,
CreativeAssignments: p.CreativeAssignments,
})
}
return adcp.MediaBuyResponse(&adcp.CreateMediaBuySuccess{
MediaBuyID: id, Status: "active", Packages: pkgs,
ValidActions: []string{"pause", "cancel", "sync_creatives", "update_packages"},
})
})
get_media_buysadcp.AddTool(server, "get_media_buys", "List media buys",
func(ctx context.Context, req *mcp.CallToolRequest, input adcp.GetMediaBuysRequest) (*mcp.CallToolResult, any, error) {
buys := []adcp.MediaBuyData{{
MediaBuyID: "mb-1", Status: "active", Currency: "USD", TotalBudget: 1000,
ValidActions: []string{"pause", "cancel", "sync_creatives", "update_packages"},
Packages: []adcp.PackageStatus{{Package: adcp.Package{PackageID: "mb-1-pkg-1", ProductID: "display", Budget: 1000}}},
}}
return adcp.MediaBuysResponse(buys, true)
})
list_creative_formats — BOTH generative and standardvar creativeFormats = []adcp.CreativeFormat{
{ // Generative — brief asset
FormatID: adcp.FormatRef{AgentURL: agentURL, ID: "display_300x250_generative"},
Name: "Generated Display 300x250",
Renders: []adcp.Render{{Width: 300, Height: 250}},
Assets: []adcp.AssetSlot{
{ItemType: "individual", AssetID: "brief", AssetType: "brief", Required: true, Description: "Creative brief"},
},
},
{ // Standard — image asset
FormatID: adcp.FormatRef{AgentURL: agentURL, ID: "display_300x250"},
Name: "Display 300x250",
Renders: []adcp.Render{{Width: 300, Height: 250}},
Assets: []adcp.AssetSlot{
{ItemType: "individual", AssetID: "image", AssetType: "image", Required: true, AcceptedMediaTypes: []string{"image/jpeg", "image/png"}},
},
},
}
adcp.AddTool(server, "list_creative_formats", "Available formats",
func(ctx context.Context, req *mcp.CallToolRequest, input adcp.ListCreativeFormatsRequest) (*mcp.CallToolResult, any, error) {
return adcp.CreativeFormatsResponse(creativeFormats, true)
})
sync_creatives — handle both brief and standardCheck format to decide status: generative → "pending_review", standard → "approved".
adcp.AddTool(server, "sync_creatives", "Submit creatives",
func(ctx context.Context, req *mcp.CallToolRequest, input adcp.SyncCreativesRequest) (*mcp.CallToolResult, any, error) {
var results []adcp.CreativeResult
for _, c := range input.Creatives {
status := "approved"
fmtID := ""
if c.FormatID != nil { fmtID = c.FormatID.ID }
if strings.Contains(fmtID, "generative") {
status = "pending_review"
}
results = append(results, adcp.CreativeResult{
CreativeID: c.CreativeID, Action: "created", Status: status,
})
}
return adcp.SyncCreativesResponse(results, true)
})
get_media_buy_deliveryUse make([]T, 0) for empty slices to ensure JSON [] not null.
adcp.AddTool(server, "get_media_buy_delivery", "Delivery metrics",
func(ctx context.Context, req *mcp.CallToolRequest, input adcp.GetMediaBuyDeliveryRequest) (*mcp.CallToolResult, any, error) {
deliveries := make([]adcp.MediaBuyDelivery, 0)
// ... populate from store
return adcp.DeliveryResponse(&adcp.DeliveryData{
ReportingPeriod: adcp.ReportingPeriod{Start: start, End: end},
MediaBuyDeliveries: deliveries,
})
})
adcp.RegisterTestController(server, &adcp.TestControllerStore{
ForceAccountStatus: func(accountID, status string) (*adcp.StateTransition, error) {
// Look up, return NOT_FOUND if missing, swap status
},
ForceMediaBuyStatus: func(mediaBuyID, status, reason string) (*adcp.StateTransition, error) {
// Same pattern. Check terminal states → INVALID_TRANSITION
},
ForceCreativeStatus: func(creativeID, status, reason string) (*adcp.StateTransition, error) { /* same */ },
SimulateDelivery: func(mediaBuyID string, p adcp.SimulateDeliveryParams) (*adcp.SimulationResult, error) { /* accumulate */ },
SimulateBudgetSpend: func(p adcp.SimulateBudgetParams) (*adcp.SimulationResult, error) { /* calculate */ },
})
package main
import (
"context"
"fmt"
"log"
"strings"
"sync"
"time"
"github.com/adcontextprotocol/adcp-go/adcp"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
const agentURL = "http://localhost:3001/mcp"
type store struct {
mu sync.RWMutex
accounts map[string]*adcp.AccountResult
mediaBuys map[string]*adcp.MediaBuyData
creatives map[string]string // id -> status
delivery map[string]*deliveryState
}
type deliveryState struct { Impressions, Clicks int; Spend float64 }
var products = []adcp.Product{ /* define with Description, PublisherProperties, FormatIDs */ }
var creativeFormats = []adcp.CreativeFormat{ /* both generative + standard */ }
func createServer(s *store) *mcp.Server {
server := mcp.NewServer(&mcp.Implementation{Name: "my-gen-seller", Version: "1.0.0"}, nil)
// Register all 9 tools + test controller
return server
}
func main() {
s := &store{accounts: make(map[string]*adcp.AccountResult), mediaBuys: make(map[string]*adcp.MediaBuyData), creatives: make(map[string]string), delivery: make(map[string]*deliveryState)}
log.Fatal(adcp.Serve(func() *mcp.Server { return createServer(s) }))
}
module your-generative-seller
go 1.25
require (
github.com/adcontextprotocol/adcp-go/adcp v0.0.0
github.com/modelcontextprotocol/go-sdk v1.5.0
)
Then go mod tidy.
go run main.go &
npx @adcp/client storyboard run http://localhost:3001/mcp media_buy_generative_seller --json
| Mistake | Fix |
|---|---|
Using mcp.AddTool directly | Use adcp.AddTool |
| Only generative formats | Must also accept standard IAB formats |
| Same status for brief and standard | Generative → "pending_review", standard → "approved" |
Products missing description | Required field |
Missing publisher_properties, format_ids, or reporting_capabilities | Required fields |
sync_governance response key results | Must be accounts |
get_delivery returns null for empty arrays | Use make([]T, 0) |
get_delivery returns null for empty deliveries | Use adcp.DeliveryResponse |
| Uppercase pricing model | Use "cpm", "cpc" not "CPM" |
| No mutex on maps | Use sync.RWMutex |
| Function | Usage |
|---|---|
adcp.AddTool(server, name, desc, handler) | Register tool |
adcp.Serve(createAgent) | HTTP server |
adcp.RegisterTestController(server, store) | Test controller |
adcp.CapabilitiesResponse(data) | Capabilities |
adcp.ProductsResponse(data) | Products |
adcp.MediaBuyResponse(*CreateMediaBuySuccess|*CreateMediaBuyError|*CreateMediaBuySubmitted) | Create media buy |
adcp.CreateMediaBuySuccessResponse(data) | Sync create media buy |
adcp.CreateMediaBuyErrorResponse(data) | Create media buy error branch |
adcp.CreateMediaBuySubmittedResponse(taskID, message) | Async create media buy |
adcp.MediaBuysResponse(buys, sandbox) | List media buys |
adcp.DeliveryResponse(data) | Delivery metrics |
adcp.SyncAccountsResponse(accounts, sandbox) | Sync accounts |
adcp.GovernanceResponse(accounts) | Sync governance |
adcp.CreativeFormatsResponse(formats, sandbox) | Creative formats |
adcp.SyncCreativesResponse(creatives, sandbox) | Sync creatives |
adcp.Result(data, summary) | Generic response |
adcp.Errorf(code, opts) | Error response |
Input types: adcp.EmptyInput, adcp.SyncAccountsRequest, adcp.SyncGovernanceRequest, adcp.GetProductsRequest, adcp.CreateMediaBuyRequest, adcp.GetMediaBuysRequest, adcp.ListCreativeFormatsRequest, adcp.SyncCreativesRequest, adcp.GetMediaBuyDeliveryRequest
The skill contains everything you need.