media_buy and creative.
The server is hosted at sales-agent.dappier.com.
Watch the Video
If you prefer a visual walkthrough, check out the accompanying video below:What is MCP?
If you’ve never seen MCP before, here’s the mental model:- The server advertises tools (name + JSON schema + description).
- The client fetches that list and lets the LLM call any tool by name with arguments that match the schema.
- The server validates, executes, and returns a result the LLM can use to continue the conversation.
What the Sales Agent MCP Server Does
Two AdCP protocols are supported:| Protocol | What it covers |
|---|---|
media_buy | Discover products, create/update/cancel campaigns (“media buys”), pull delivery data. |
creative | List creative formats, create/update Dappier Brand Agent creatives, list existing creatives. |
- An AI-callable surface for Dappier Sponsored Conversations — the branded prompt suggestions that appear inside publisher AI chat widgets across the Dappier network.
- Campaign lifecycle in a single tool call: create a paused campaign with advertiser details, CTA, targeting, and optional sponsored prompts.
- Creative lifecycle: attach a configurable Dappier Brand Agent (conversational AI creative) to a campaign via
build_creative. - Delivery reporting over a date range.
- It does not quote prices or sell impressions directly — pricing is handled offline by Dappier sales (
[email protected]). - It does not activate campaigns. Every campaign is born
paused; a Dappier reviewer must approve and a sales rep must complete Google Ad Manager (GAM) line-item setup before it serves. - It does not honor standard AdCP targeting (geo, device, language, audience). Dappier targeting is
dappier_network/my_network/individual_agents.
Getting Started
Base URLs and Endpoints
The server is hosted by Dappier. Connect using the standard MCP streamable-HTTP transport.| Transport | Path | When to use |
|---|---|---|
| Streamable HTTP | POST /mcp | All MCP clients (Claude.ai, Cursor, custom apps, mcp-remote proxy). |
| Path | Returns |
|---|---|
GET / | HTML landing page confirming the worker is up. |
GET /.well-known/mcp.json | The AdCP server card (name, version, protocols, tool names). |
GET /.well-known/server.json | Same server card (alias). |
Use the deployed URL Dappier publishes — for example
https://sales-agent.dappier.com/mcp. Confirm the current host with your Dappier contact.Authentication
Every MCP request (except/ and /.well-known/* discovery endpoints) requires a Dappier API key.
Query parameter
HTTP header
401 with a plain-text body:
Getting an API key
Create Dappier API keys at platform.dappier.com/profile/api-keys. The key is used:- As a Bearer token on every outbound call the server makes to
api.dappier.com. - To authorize the MCP session itself at the edge.
Connecting from Common MCP Clients
Claude Desktop (via mcp-remote)
Claude Desktop speaks MCP over stdio. To reach a remote HTTPS MCP server, proxy through mcp-remote.
Edit your Claude Desktop config (Settings → Developer → Edit Config):
Claude.ai (Connectors / Remote MCP)
On claude.ai, add a custom connector / remote MCP server pointing athttps://sales-agent.dappier.com/mcp. Supply the API key via header (dappier-api-key) where the UI allows custom headers, or via ?apiKey=... in the URL otherwise.
Cursor
Add a remote MCP server in Cursor’s MCP settings pointing at:Cloudflare AI Playground
Go to playground.ai.cloudflare.com and enter this as the server URL:Custom Node.js Client
Any client built on@modelcontextprotocol/sdk can connect over streamable HTTP:
TypeScript
Anthropic SDK (Native MCP Connector)
The Anthropic Messages API can call a remote MCP server directly as a tool-use source — no separate MCP client SDK required:Python
Server Discovery
GET /.well-known/mcp.json (and /.well-known/server.json) returns:
Conventions
ID prefixes
| Prefix | Meaning | Example |
|---|---|---|
cp_ | Dappier campaign / media buy id | cp_01HW9ZAB... |
am_ | Dappier Brand Agent / creative id | am_01HW9CD... |
pm_ | Sponsored prompt id | pm_01ABC... |
Schemas reject values that don’t match the expected prefix — the server won’t silently accept a mismatched id.
Response envelope
Every tool returns an MCPCallToolResult with two fields:
content[0]— atextblock containing pretty-printed JSON (for LLMs reading the text).structuredContent— the same payload as a machine-readable object (for programmatic clients).
Error shape
Errors follow AdCP conventions:status: "failed" wrapper is used:
Error code reference
| Code | Meaning |
|---|---|
INVALID_REQUEST | Generic 400-level rejection on create. |
VALIDATION_ERROR | Update with no fields, or 400-level rejection on update. |
INVALID_DATE_RANGE | Missing, malformed, or unsorted start_date / end_date. |
POLICY_VIOLATION | 401/403 (bad key, insufficient scope). |
AUTH_REQUIRED | 401 on read tools. |
MEDIA_BUY_NOT_FOUND | Unknown cp_xxxx. |
CREATIVE_NOT_FOUND | Unknown am_xxxx. |
INVALID_STATE | Campaign in a state that forbids the requested action. |
CREATIVE_ID_EXISTS | 409 conflict (duplicate creative attached). |
CONTEXT_REQUIRED | Default-scope list returned zero; supply media_buy_ids or status_filter. |
VERSION_UNSUPPORTED | adcp_major_version not supported. |
INTERNAL_ERROR | Fallback. |
Context passthrough
Most tools accept acontext: Record<string, unknown> field. Whatever you send is echoed back unchanged on the response — useful for correlating tool calls with your own session state.
Typical campaign lifecycle
Tools Reference
get_adcp_capabilities
Purpose: The first call a buyer should make. Tells you which AdCP protocols Dappier implements, which features inside those protocols are honored vs. ignored, the auth model, and which creative capabilities are available.
Network behavior: In-memory lookup. No outbound call. Returns instantly.
Inputs (all optional)
| Field | Type | Description |
|---|---|---|
adcp_major_version | integer | AdCP major version the buyer’s payloads conform to. If provided and unsupported, returns VERSION_UNSUPPORTED. Currently supported: 1. |
protocols | array of "media_buy" | "signals" | "governance" | "sponsored_intelligence" | "creative" | "compliance_testing" | Filter which protocols you want capability info for. Omit to get everything Dappier supports. |
Success response
Error response
Targeting dimensions declared
false will return a validation error if sent — they are not silently dropped. media_buy and creative are the only protocols you’ll see.list_creative_formats
Purpose: Discover the creative formats supported by the Dappier sales agent. Call this before build_creative.
Network behavior: In-memory. Instant.
Inputs
All inputs are optional and all are ignored in v1 — accepted for AdCP forward-compat:| Field | Type | Notes |
|---|---|---|
format_ids | array of { agent_url: string, id: string } | Ignored. |
asset_types | string[] | Ignored. |
max_width, max_height, min_width, min_height | integer | Ignored. |
is_responsive | boolean | Ignored. |
name_search | string | Ignored. |
wcag_level | "A" | "AA" | "AAA" | Ignored. |
pagination.max_results, pagination.cursor | Ignored. |
Success response
Returns a single format descriptor fordappier_brand_agent (the only creative format Dappier exposes). The format_id.agent_url returned here is the exact value you must pass into build_creative.target_format_id.
Call this tool to pick up the exact
agent_url string — don’t hard-code it.build_creative
Purpose: Create a new Dappier Brand Agent (conversational AI creative) and attach it to a campaign, or update an existing one.
A Brand Agent is a configurable chat experience branded for an advertiser: name, description, optional persona, optional single knowledge source (RSS feed or webpage), and optional widget overrides (logo, colors, welcome copy, theme).
Two modes
| Mode | How to trigger | Effect |
|---|---|---|
| CREATE | Omit creative_id | Creates a new Brand Agent, attaches it to media_buy_id. Requires creative_manifest.assets.name and ...description. The campaign must not already have a creative attached. |
| UPDATE | Supply creative_id (am_xxxx) | PATCH semantics — only the fields you send change. source_url and source_type must be supplied together or not at all. |
Inputs
| Field | Type | Required | Notes |
|---|---|---|---|
target_format_id | { agent_url: URL, id: "dappier_brand_agent" } | yes | Must be dappier_brand_agent. Get agent_url from list_creative_formats. |
media_buy_id | cp_xxxx string | yes | Campaign to attach the Brand Agent to. |
creative_id | am_xxxx string | no | Present → UPDATE. Omit → CREATE. |
creative_manifest.format_id | { agent_url, id: "dappier_brand_agent" } | yes | Must equal target_format_id. |
creative_manifest.assets.name | string ≤120 | required on CREATE | Shown as “Ask {name}”. |
creative_manifest.assets.description | string ≤500 | required on CREATE | Brand description used inside the conversation. |
creative_manifest.assets.persona | string | no | Free-form tone instruction. |
creative_manifest.assets.source_url | URL | no | RSS feed or webpage. Must pair with source_type. |
creative_manifest.assets.source_type | "rss" | "webpage" | no | Must pair with source_url. |
creative_manifest.assets.logo_url | URL | no | Widget logo override. |
creative_manifest.assets.primary_color | string | no | Hex color, e.g. #8353E2. |
creative_manifest.assets.welcome_title | string | no | Title above the Ask-AI widget. |
creative_manifest.assets.welcome_description | string | no | Subtitle above the widget. |
creative_manifest.assets.placeholder_text | string | no | Placeholder inside the input box. |
creative_manifest.assets.theme_mode | "light" | "dark" | no | Widget theme. |
context | Record<string, unknown> | no | Echoed back. |
Success response
Example — CREATE
Example — UPDATE (change logo only)
list_creatives
Purpose: Browse the tenant’s Brand Agents, or look up specific ones.
Three filter paths
Mutually exclusive on the primary id filters:| Path | How to trigger | Behavior |
|---|---|---|
| By creative ids | filters.creative_ids | Look up specific Brand Agents. Missing ids → CREATIVE_NOT_FOUND entries in errors[]. Pagination ignored. |
| By media buy ids | filters.media_buy_ids | List Brand Agents attached to specific campaigns. Missing campaigns → MEDIA_BUY_NOT_FOUND. Pagination ignored. |
| Default | omit both | Paginated list of every tenant Brand Agent. |
If both
creative_ids and media_buy_ids are supplied, creative_ids wins.filters.name_contains (case-insensitive) and filters.statuses (approved / pending_review / archived) narrow any of the three paths.
Inputs
| Field | Type | Notes |
|---|---|---|
filters.creative_ids | am_xxxx[] | Lookup by id. |
filters.media_buy_ids | cp_xxxx[] | Lookup by campaign. |
filters.name_contains | string | Case-insensitive substring. |
filters.statuses | ("approved" | "pending_review" | "archived")[] | |
pagination.max_results | int 1-100 (default 50) | Honored only on default path. |
pagination.cursor | string | Opaque — round-trip verbatim. |
context | object | Echoed back. |
get_products
Purpose: Discover Dappier’s advertising inventory. Always returns the single Sponsored Conversations product (or an empty list if your filters exclude it).
Network behavior: In-memory. No API call.
Inputs
| Field | Type | Required | Notes |
|---|---|---|---|
buying_mode | "brief" | "wholesale" | "refine" | yes | How you want results selected. |
brief | string | required iff buying_mode === "brief" | Natural-language campaign description. Must be omitted otherwise. |
brand.domain | string | no | E.g. "acmepetfood.com". |
brand.brand_id | string | no | Optional stable id. |
filters.delivery_type | "guaranteed" | "non_guaranteed" | no | Dappier only offers non_guaranteed; anything else returns []. |
filters.channels | string[] | no | Must include "native" (or be omitted) for the product to match. |
filters.format_ids | { agent_url, id }[] | no | Must include Dappier’s format to match. |
Success response
brief_relevance is present only when brief was supplied.create_media_buy
Purpose: Create a sponsored campaign on the Dappier network. Campaign is born paused. It will not serve until:
- Dappier sales sets up the GAM line item offline.
- A Dappier reviewer approves it.
sponsored_prompts to create the clickable prompt content atomically with the campaign.
Inputs
| Field | Type | Required | Notes |
|---|---|---|---|
brand.name | string | yes | → Dappier details.advertiser_name. |
brand.domain | string | no | |
buyer_ref | string | no | Your correlation id, echoed in the response. |
packages | Package[] | defaulted | Defaults to a single sponsored_conversations package. Only set it to pass targeting_overlay.product_ids for individual-agent targeting. |
packages[].product_id | literal "sponsored_conversations" | defaulted | Only allowed value. |
packages[].pricing_option_id | literal "contact_sales" | defaulted | Only allowed value. |
packages[].format_ids[] | { agent_url, id: "sponsored_prompt_standard" } | defaulted | |
packages[].targeting_overlay.product_ids | string[] | no | am_xxxx agent ids. Only honored when targeting_type === "individual_agents". |
start_time | ISO 8601 datetime | no | |
end_time | ISO 8601 datetime | no | |
campaign_name | string | yes | → Dappier details.name. |
cta_link | URL | yes | Where clicks land. Must be supplied by the user — never infer from brand name / domain. |
cta_button_text | string | yes | E.g. "Shop now", "Learn more". Must be supplied by the user — never invent. |
targeting_type | "dappier_network" | "my_network" | "individual_agents" | defaulted | Default: dappier_network. |
sponsored_prompts[] | { prompt_text (≤200), thumbnail?, is_active (default true) } | no | Any caller-supplied prompt_id is silently dropped on create — the server assigns new pm_xxxx ids. |
context | object | no | Echoed back. |
Success response
Error response
INVALID_REQUEST, 401/403 → POLICY_VIOLATION, 409 → CREATIVE_ID_EXISTS, else → INTERNAL_ERROR.
Example
update_media_buy
Purpose: Modify an existing Dappier campaign. PATCH semantics — omitted fields are preserved. Atomic — either all changes apply or none do.
The handler routes to one of three backend operations based on what you send:
| Request shape | Backend action |
|---|---|
canceled: true (any other field optional) | Soft-delete the campaign. Irreversible. Overrides other changes. |
Only paused set, no other update fields | Status-only flip (pause/resume). |
Any update field (optionally plus paused) | Full update (all provided fields). |
Inputs
All optional exceptmedia_buy_id:
| Field | Type | Notes |
|---|---|---|
media_buy_id | cp_xxxx | Required. |
paused | boolean | true = pause, false = resume. Resume may be rejected until Dappier sales approves. |
canceled | literal true | Soft-delete. Irreversible. |
cancellation_reason | string | Echoed in the response message. |
start_time, end_time | ISO 8601 | New flight dates. |
campaign_name, advertiser_name, brand_description, contextual_keywords | string | Map to details.*. |
targeting_type | "dappier_network" | "my_network" | "individual_agents" | |
product_ids | am_xxxx[] | Replaces details.targeting.agent_list. |
cta_link | URL | Only include if the user explicitly asked to change it. Otherwise omit (existing value preserved). |
cta_button_text | string | Only include if the user explicitly asked. |
custom_followup_prompts | string[] (max 3) | Replaces existing list. |
sponsored_prompts[] | { prompt_id?, prompt_text, thumbnail?, is_active } | Full-replacement semantics — see below. |
context | object | Echoed back. |
Sponsored-prompts replacement semantics
Pass the full desired set after the update:- Entries with
prompt_id(pm_xxxx) → updated. - Entries without
prompt_id→ newly created. - Existing prompts not present in the array → soft-deleted.
Success response
Resume-pending special case
If you sendpaused: false and the backend rejects with INVALID_STATE, NOT_APPROVED, or PENDING_APPROVAL, the tool returns status: "submitted" (not an error) with the message:
Resume requires Dappier sales to complete GAM setup and a reviewer to approve. The campaign remains paused.
Validation error — empty request
If you send nocanceled, no paused, and no update fields:
Examples
Pause a campaign:get_media_buys
Purpose: Retrieve the current operational state of Dappier campaigns — configuration, sponsored-prompt approval status, and which campaigns are waiting on Dappier sales / GAM setup.
Use this for “what’s the current state of my campaigns?”. For performance over a date range, use get_media_buy_delivery instead.
Inputs (all optional)
| Field | Type | Notes |
|---|---|---|
media_buy_ids | cp_xxxx[] | Fetch specific campaigns in parallel. When set, no implicit status filter is applied. |
status_filter | one of / array of "pending_creatives" | "pending_start" | "active" | "paused" | "completed" | "rejected" | "canceled" | Defaults to ["active"] when neither media_buy_ids nor status_filter is provided. |
include_snapshot | boolean (default false) | Accepted for AdCP compliance. Always returns snapshot_unavailable_reason: "SNAPSHOT_UNSUPPORTED". |
pagination.max_results | int 1-100 (default 50) | |
pagination.cursor | string | Opaque. Currently encoded as page:<n>. |
context | object | Echoed back. |
Success response
Status derivation rules
is_deleted === true→canceled- else
status === "active"→active - else if creative_ids exist →
paused - else →
pending_creatives
Valid actions by status
| Status | Actions |
|---|---|
pending_creatives | cancel, update_media_buy |
active | pause, cancel, update_budget, update_dates, update_media_buy |
paused | resume, cancel, update_budget, update_dates, update_media_buy |
canceled | — |
Empty-result nudge
If you pass neithermedia_buy_ids nor status_filter, the default filter is ["active"]. If that produces no campaigns, the response includes:
get_media_buy_delivery
Purpose: Retrieve delivery metrics (impressions, clicks, etc.) for Dappier campaigns over a date range or campaign lifetime.
Use this for “how did my campaigns perform over a period?”. For current state, use get_media_buys.
Inputs
| Field | Type | Notes |
|---|---|---|
media_buy_ids | cp_xxxx[] | When set, no implicit status filter is applied. |
status_filter | one of / array of "pending_creatives" | "pending_start" | "active" | "paused" | "completed" | Defaults to ["active"] server-side when neither media_buy_ids nor status_filter is provided. |
start_date | YYYY-MM-DD | Inclusive. Must be paired with end_date. |
end_date | YYYY-MM-DD | Exclusive. Must be paired with start_date. |
context | object | Echoed back. |
Date range rules
- Send both dates or neither.
- Both must match
YYYY-MM-DD. start_date < end_datestrictly.- Violations return
INVALID_DATE_RANGEwithfieldset to the offender — no backend call is made.
Unsupported in v1
- Reporting dimensions (geo, device, audience, placement).
- Spend, ROAS, CPM, conversion value — pricing is handled offline in GAM, not tracked in Dappier.
Success response
Whatever the backend returns for the AdCP delivery shape is passed through verbatim, plus yourcontext echo. Expect at minimum per-campaign impressions/clicks and a reporting_period object.
Error response
code / error_code passed through if provided, else 404 → MEDIA_BUY_NOT_FOUND, 401 → AUTH_REQUIRED, 400 → INVALID_DATE_RANGE, else → INTERNAL_ERROR.
End-to-End Recipes
Launch a campaign (minimum viable flow)
What’s happening with my campaigns right now?
How did last month perform?
Edit the prompts on an existing campaign
Any pre-existing prompts not listed above are soft-deleted.
Attach a different Brand Agent to a campaign
Callbuild_creative in UPDATE mode with the existing creative_id. Only send the asset fields you want to change — the rest are preserved.
Guardrails the Server Enforces
These are quiet-but-strict rules callers often trip over.| Rule | Where |
|---|---|
Never infer cta_link or cta_button_text from the brand. Ask the user. The schema descriptions say so explicitly and the backend has no way to recover. | create_media_buy, update_media_buy |
creative_id starts with am_, media_buy_id starts with cp_, prompt ids with pm_. | Everywhere |
source_url and source_type must be supplied together. | build_creative |
brief is required iff buying_mode === "brief"; forbidden otherwise. | get_products |
start_date and end_date must both be present or both absent; start_date < end_date. | get_media_buy_delivery |
update_media_buy with no fields at all returns VALIDATION_ERROR. | update_media_buy |
custom_followup_prompts max 3 items. | update_media_buy |
Any targeting dimension declared false in capabilities will reject rather than silently drop. | create_media_buy, update_media_buy |
FAQ
Do I need to call get_adcp_capabilities on every request?
Do I need to call get_adcp_capabilities on every request?
No. Call it once per session to discover the surface, then cache the result.
Why is my new campaign still paused an hour later?
Why is my new campaign still paused an hour later?
Every Dappier campaign is born paused and requires (a) Dappier sales to configure the GAM line item offline and (b) a reviewer to approve. Calling
update_media_buy { paused: false } before those steps complete returns status: "submitted" with an explanatory message, not an error.Can I set a budget?
Can I set a budget?
Pricing is offline. Budget / CPM / ROAS fields are not honored.
pricing_option_id is always "contact_sales".Can I target by geo / device / audience?
Can I target by geo / device / audience?
No. Dappier targeting is
dappier_network (whole network), my_network (your own publishers), or individual_agents (list of am_xxxx ids).What happens if I pass extra fields the server doesn't understand?
What happens if I pass extra fields the server doesn't understand?
Fields defined by AdCP but unused by Dappier (
idempotency_key, budget, include_snapshot, include_history, reporting_dimensions, format-filter arguments, etc.) are accepted for forward compatibility but ignored. Fields that are targeting dimensions Dappier doesn’t support will cause validation errors.How do I correlate a tool call with my own request id?
How do I correlate a tool call with my own request id?
Pass
context: { your_request_id: "..." } on any tool — it’s echoed back unchanged.Is the cursor format stable?
Is the cursor format stable?
get_media_buys currently uses page:<n> cursors. Treat it as opaque — don’t parse or construct it yourself.Can I create multiple creatives on one campaign?
Can I create multiple creatives on one campaign?
No. On CREATE, the target campaign must not already have a creative attached. Use UPDATE mode to modify the existing Brand Agent.
Glossary
| Term | Meaning |
|---|---|
| AdCP | Advertising Context Protocol — an open spec for AI-callable ad-platform tools. |
| MCP | Model Context Protocol — the transport/framing standard this server speaks. |
| Media buy | An AdCP term for a campaign. One Dappier campaign = one media buy. |
| Package | An AdCP subdivision of a media buy. Dappier has no native package model — each campaign is returned with a single synthetic package. |
| Sponsored Conversations | Dappier’s only product. A branded prompt pill that opens into a full Brand Agent conversation. |
| Brand Agent | The conversational AI creative attached to a campaign. Id prefix am_. |
| Sponsored prompt | The clickable prompt text shown to users. Id prefix pm_. |
| GAM | Google Ad Manager — where Dappier sales configures line items offline before a campaign can serve. |
| Dappier network | The set of publishers running Dappier AI chat widgets. |
Conclusion
The Dappier Sales Agent MCP gives AI agents a complete, AI-callable surface over the Dappier Sponsored Conversations network — campaign creation, Brand Agent creatives, and delivery reporting — while keeping pricing and activation in the hands of Dappier’s sales team. 🔗 Explore further:- Dappier Developers
- Dappier Platform — create your API key
- AdCP Specification
- Model Context Protocol
- Questions on pricing or activation? Contact [email protected]

