An open-source implementation of the Ad Context Protocol (AdCP) sales-agent role, maintained under Prebid.org. It lets AI agents discover and buy advertising inventory through standardized MCP and A2A interfaces.
Status: alpha. A pre-1.0 implementation of a pre-release protocol (AdCP 3.1.1), under active development. APIs still change between releases —
v2.0.0introduced breaking changes — so pin a version and expect to adapt on upgrade. It is one of several AdCP sales-agent implementations, not a sole or canonical reference. The codebase is substantial and functional (Google Ad Manager integration, multi-tenant isolation, 16 AdCP tools, extensive tests), but treat it as alpha and validate before relying on it.
The Prebid Sales Agent is a server that:
- Exposes advertising inventory to AI agents via MCP (Model Context Protocol) and A2A (Agent-to-Agent)
- Integrates with ad servers like Google Ad Manager
- Provides an admin interface for managing inventory and campaigns
- Handles the full campaign lifecycle from discovery to reporting
This implementation targets AdCP spec version 3.1.1 via the adcp==6.6.0
Python SDK. The protocol is pre-1.0 — request/response shapes still move between
spec releases, and SDK bumps can change them. See
docs/adcp-spec-version.md for the SDK-to-spec mapping
and bump procedure. The pin is enforced by a CI guard
(tests/unit/test_adcp_spec_version.py), which fails on drift.
| I want to... | Start here |
|---|---|
| Deploy my own sales agent (publisher) | Quickstart Guide |
| Evaluate or develop locally | Quick Start below |
| Run a multi-tenant platform | Deployment Guide |
Try the sales agent locally:
# Clone and start
git clone https://github.com/prebid/salesagent.git
cd salesagent
docker compose up -d
# Test the MCP interface
uvx adcp http://localhost:8000/mcp/ --auth test-token list_tools
uvx adcp http://localhost:8000/mcp/ --auth test-token get_products '{"brief":"video"}'Access services at http://localhost:8000:
- Admin UI:
/adminor just click "Log in to Dashboard" (test credentials:test123) - MCP Server:
/mcp/ - A2A Server:
/a2a
For production deployment, see the Quickstart Guide.
Publishers deploy their own sales agent. Choose based on your needs:
| Platform | Time | Difficulty | Guide |
|---|---|---|---|
| Docker (local/on-prem) | 2 min | Easy | quickstart.md |
| Fly.io (cloud) | 10-15 min | Medium | fly.md |
| Google Cloud Run | 15-20 min | Medium | gcp.md |
Docker is the fastest - it bundles PostgreSQL and just works. Cloud platforms require separate database setup.
The app itself is platform-agnostic — beyond the walkthroughs above you can host it anywhere:
- Docker (recommended) — any Docker-compatible platform
- Kubernetes — full k8s manifests supported
- Cloud providers — AWS, GCP, Azure, DigitalOcean
- Platform services — Fly.io, Heroku, Railway, Render
See docs/deployment/ for platform-specific guides.
Because this is alpha software tracking a beta protocol, pin the version you deploy and re-test after every upgrade — minor releases can carry breaking changes.
Configure via the Admin UI:
- Configure your ad server (Settings → Adapters)
- Set up products that match your GAM line items
- Add advertisers who will use the MCP API
- Set your custom domain (Settings → General)
git clone https://github.com/prebid/salesagent.git
cd salesagent
make setup # One command: installs deps, starts Docker, verifies healthSee the Getting Started guide for prerequisites, manual setup steps, testing workflows, and common operations.
For GAM integration, choose your authentication method:
Service Account (Recommended for Production):
- No OAuth credentials needed
- Configure service account JSON in Admin UI
- See GAM Adapter Guide for setup
OAuth (Development/Testing):
- Create OAuth credentials at Google Cloud Console
- Add to .env:
GAM_OAUTH_CLIENT_ID=your-client-id.apps.googleusercontent.com GAM_OAUTH_CLIENT_SECRET=your-client-secret
- Configure in Admin UI: Settings → Adapters → Google Ad Manager
Add to your Claude config (~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"adcp": {
"command": "uvx",
"args": ["mcp-remote", "http://localhost:8000/mcp/", "--header", "Authorization: Bearer YOUR_TOKEN"]
}
}
}Your token is shown once, when the advertiser is created in Admin UI → Advertisers; rotate it there if it is lost.
Container won't start?
docker compose logs adcp-server | head -50GAM OAuth error?
- Verify
GAM_OAUTH_CLIENT_IDandGAM_OAUTH_CLIENT_SECRETin.env - Restart:
docker compose restart
More help: Troubleshooting Guide
- Quickstart - Docker deployment (2 min)
- Fly.io - Cloud deployment (10-15 min)
- Google Cloud Run - GCP deployment (15-20 min)
- Single-Tenant - Single publisher deployment
- Multi-Tenant - Platform deployment
- Development Guide - Local development and contributing
- Architecture - System design and database schema
- Troubleshooting Guide - Monitoring and debugging
The MCP server exposes 16 AdCP tools (registered in
src/core/main.py). The server also exposes AdCP operations over A2A as JSON-RPC skills; the exact set differs slightly from the MCP tool list.
| Area | Tools |
|---|---|
| Discovery | get_products, list_creative_formats, list_authorized_properties, get_adcp_capabilities |
| Media buys | create_media_buy, update_media_buy, get_media_buys, get_media_buy_delivery, update_performance_index |
| Creatives | sync_creatives, list_creatives |
| Tasks / workflow | list_tasks, get_task, complete_task |
| Accounts | list_accounts, sync_accounts |
Adapters live in src/adapters/, are registered in
src/adapters/__init__.py, and are selected per tenant. The adapter interface is
defined in src/adapters/base.py. Maturity varies — GAM is the most complete;
the others are at earlier stages.
| Adapter | Key(s) | Notes |
|---|---|---|
| Google Ad Manager | gam, google_ad_manager |
The most developed adapter (src/adapters/gam/). Supports CPM, VCPM, CPC, and FLAT_RATE pricing with automatic line-item-type selection. See docs/adapters/. |
| Broadstreet | broadstreet |
Broadstreet integration (src/adapters/broadstreet/), with Admin UI configuration. |
| Kevel | kevel |
Kevel integration (src/adapters/kevel.py). |
| Triton Digital | triton, triton_digital |
Triton Digital integration (src/adapters/triton_digital.py). |
| Mock | mock |
Simulated ad server for testing and local development (src/adapters/mock_ad_server.py). Supports all AdCP pricing models; zero real spend. |
For AI agents
- Natural-language product discovery
- Media-buy creation, updates, and delivery reporting
- Creative sync and listing with approval workflows
For publishers
- Multi-tenant isolation (data scoped per publisher)
- Adapter pattern for multiple ad servers
- Real-time activity dashboard (Server-Sent Events)
- Human-in-the-loop workflow/approval system
- Audit logging of operations
- Admin web UI with Google OAuth
For developers
- MCP interface (FastMCP, HTTP/SSE transport)
- A2A interface (JSON-RPC 2.0)
- REST API for tenant management
- Docker-based local and production deployment
- Unit, integration, e2e, admin, BDD, and UI test suites
The primary interface for AI agents. Built with FastMCP over HTTP/SSE transport.
JSON-RPC 2.0 server for agent-to-agent communication:
- Endpoint:
/a2a(also served on port 8091) - Discovery:
/.well-known/agent.json(also/.well-known/agent-card.json,/agent.json) - Authentication: Bearer tokens via Authorization header
- Library: Built with
a2a-sdk[http-server]
The mock ad server (src/adapters/mock_ad_server.py) serves a tenant configured with
the mock adapter and spends nothing. It answers every AdCP tool with simulated
inventory and delivery, and it honours two testing affordances that live in the data
rather than in request headers:
- Keyword scenarios: a media buy or creative whose name carries
[REJECT:reason],[APPROVE], or[ASK:field]is rejected, approved, or sent to human review (src/adapters/test_scenario_parser.py). - Seeded delivery:
src/services/delivery_simulator.pywrites per-buy delivery rows thatget_media_buy_deliveryreturns verbatim, so an end-to-end test can pin exact numbers.
Requests carry no testing headers. A request is served the same way whether a test or a buyer sent it; the only difference between them is the tenant's adapter.
from fastmcp.client import Client
from fastmcp.client.transports import StreamableHttpTransport
# Connect to the server
transport = StreamableHttpTransport(
url="http://localhost:8000/mcp/",
headers={"Authorization": "Bearer your_token"},
)
client = Client(transport=transport)
async with client:
# 1. Discover products. Each product carries one or more `pricing_options`.
products = await client.tools.get_products(brief="video ads for sports content")
# 2. Book a media buy. Each package references a product and one of its
# pricing options. `idempotency_key` is REQUIRED (16-255 chars) — reusing
# the same key makes retries safe (returns the original buy, no duplicate).
result = await client.tools.create_media_buy(
brand="acme.com", # domain shorthand for a BrandReference
start_time="2026-08-01T00:00:00Z", # ISO 8601, or "asap"
end_time="2026-08-31T23:59:59Z",
packages=[
{
"product_id": "ctv_sports",
"pricing_option_id": "cpm_usd", # from product.pricing_options
"budget": 50000, # amount in the pricing option's currency
}
],
idempotency_key="acme-ctv-2026-08-0001",
)salesagent/
├── src/ # Source code
│ ├── core/ # Core MCP server components
│ │ ├── main.py # MCP server + tool registration
│ │ ├── schemas/ # Pydantic models (AdCP-compliant; package)
│ │ ├── tools/ # Tool _impl functions + wrappers (package)
│ │ ├── database/ # SQLAlchemy models, session, repositories
│ │ ├── config_loader.py # Configuration management
│ │ └── audit_logger.py # Security and audit logging
│ ├── services/ # Business logic services
│ ├── adapters/ # Ad server integrations
│ │ ├── base.py # Base adapter interface
│ │ ├── gam/ # Google Ad Manager adapter
│ │ ├── broadstreet/ # Broadstreet adapter
│ │ └── mock_ad_server.py # Mock adapter
│ ├── a2a_server/ # A2A (agent-to-agent) server
│ └── admin/ # Admin UI (Flask)
├── scripts/ # Setup, dev, ops, and deploy scripts
├── tests/ # unit / integration / e2e / admin / bdd
├── docs/ # Documentation
├── examples/ # Example code
├── alembic/ # Database migrations
├── templates/ # Jinja2 templates
└── config/ # Configuration files (incl. nginx/)
- Python 3.12+
- Docker and Docker Compose (for easy deployment)
- PostgreSQL (Docker Compose handles this automatically)
- Google OAuth credentials (for Admin UI)
- Gemini API key (for AI-powered product discovery)
We welcome contributions! Please see our Development Guide for:
- Setting up your development environment
- Running tests
- Code style guidelines
- Creating pull requests
When contributing, follow the standardized database patterns. All data access goes
through SQLAlchemy 2.0 ORM via repository classes — see the
Patterns reference and CLAUDE.md for details.
# Use a context-managed session
from src.core.database.database_session import get_db_session
with get_db_session() as session:
# Your database operations
session.commit()Users can belong to multiple tenants with the same email address (like GitHub, Slack, etc.):
- Sign up for multiple publisher accounts with one Google login
- Different roles per tenant (admin in one, viewer in another)
- Users are tenant-scoped via a composite unique constraint
(tenant_id, email)
Deactivate test or unused tenants without losing data:
- All data preserved (media buys, creatives, principals)
- Hidden from login and tenant selection; API access blocked
- Reactivatable by a super admin
New users can self-provision tenants:
- Google OAuth authentication
- GAM-only for self-signup (other adapters via support)
- Auto-creates tenant, user, and default principal
- Available at
/signupon the main domain
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Documentation: docs/
Apache 2.0 License - see LICENSE file for details.
- AdCP Specification - Protocol specification
- Model Context Protocol - MCP tools and SDKs
- FastMCP - MCP server framework