Skip to main content
The Dappier Sales Agent MCP lets marketers use AI agents to create conversational Brand Agents and launch advertising campaigns. Brand Agents can run across:
  • Dappier Ask AI inventory across our publisher network
  • Open internet advertising inventory through existing programmatic buying platforms / DSPs
Using the MCP, AI agents can discover available inventory, create and launch campaigns, manage campaigns, and pull performance data. Dappier Sales Agent is built on the Advertising Context Protocol (AdCP) and is available at sales-agent.dappier.com.

Watch the Video

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

Dappier is a sales agent on the AdCP media_buy protocol, selling one product: Sponsored Conversations. A sponsored prompt — a clickable suggested question — appears inside publisher AI chat widgets across the Dappier network. When a user clicks it, they enter a full Brand Agent conversation: an AI chat experience that answers as the advertiser’s brand, grounded in the brand’s own content, with follow-up prompts and a call-to-action button. What you get as a developer:
  1. An AI-callable surface for Dappier Sponsored Conversations — the branded prompt suggestions that appear inside publisher AI chat widgets across the Dappier network.
  2. One call to launch. create_media_buy builds the brand agent, the campaign, and the branded chat widget together. There is no separate creative-upload step.
  3. A shareable preview (demo_url) and two ready-to-traffic activation snippets returned on that same call.
  4. Delivery reporting and a feedback channel back to Dappier.
What the server is not:
  • It does not quote prices. Sponsored Conversations are flat-rate; the figure is agreed offline with Dappier.
  • It does not activate campaigns. Every campaign is registered paused, with confirmed_at: null, pending Dappier trafficking and reviewer activation.
  • It does not honor standard AdCP targeting (geo, device, language, audience). Contextual keywords are the only targeting signal — every campaign runs across the full Dappier network.
  • It has no creative library. Creatives are inline and package-scoped, not reusable by creative_id.
  • It cannot cancel a buy over the API, and it cannot change flight dates after creation.
Earlier versions of this server exposed separate build_creative and list_creatives tools. Those are removed — creative creation is folded into create_media_buy, and creative revision into update_media_buy.

Getting Started

Base URLs and Endpoints

The server is hosted by Dappier. Connect using the standard MCP streamable-HTTP transport. Health / discovery endpoints (open, no auth):

Authentication

Every MCP request (except / and /.well-known/* discovery endpoints) requires a Dappier API key.

Query parameter

HTTP header

If the key is missing you’ll get an HTTP 401 with a plain-text body:

Getting an API key

Create Dappier API keys at platform.dappier.com/profile/api-keys. Keys start with ak_. 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.
Keep the key server-side or in a secret manager. Never expose it in browser code.

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):
Restart Claude Desktop — the 8 Dappier tools will appear in the tool picker.

Claude.ai (Connectors / Remote MCP)

On claude.ai, add a custom connector / remote MCP server pointing at https://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:
Cursor supports the streamable HTTP transport directly.

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 Claude Messages API can call a remote MCP server directly as a tool-use source — no separate MCP client SDK required:
Python
Both halves are required — mcp_servers alone is rejected as a validation error. The mcp_server_name in tools must match the name in mcp_servers.

Server Discovery

GET /.well-known/mcp.json (and /.well-known/server.json) returns:
Only media_buy is declared. list_creative_formats is a Creative Protocol task, but the spec lists it in the media-buy sales agent’s required set — implementing it does not oblige a creative claim, and none of the creative-lifecycle tasks are implemented.

Conventions

Response envelope

Every tool returns an MCP CallToolResult with two fields:
  • content[0] — a text block containing pretty-printed JSON (for LLMs reading the text).
  • structuredContent — the same payload as a machine-readable object (for programmatic clients).
Both contain the same data — use whichever matches your client. Inside that payload, every response carries the AdCP envelope:
status is "completed" on success and "failed" on error. Reads and writes alike complete synchronously — this agent never returns a pending task.

Idempotency

idempotency_key is required on the three tools with side effects — create_media_buy, update_media_buy, and provide_performance_feedback — and accepted-but-ignored on the read tools.
  • Format: 16–255 characters of letters, digits, and _ . : - only. Use a UUID v4.
  • Retrying with the same key replays the original response, and the response carries replayed: true.
  • A fresh key always creates a new campaign and a new brand agent. This is the most common way duplicate buys get made.
  • Replay window: 24 hours (replay_ttl_seconds: 86400).

Optimistic concurrency (revision)

update_media_buy requires the revision you last observed via get_media_buys. It is checked atomically; a stale value returns CONFLICT and changes nothing. Re-read the buy and retry with the current revision and a fresh idempotency_key.

The account object

account is a strict one-of. Send either { account_id } or { brand, operator } — never fields from both, which is rejected rather than reconciled.
Most callers use { brand, operator }; no prior setup call is needed. operator is the entity placing the buy on the brand’s behalf — an agency trading desk, or the brand’s own domain when buying direct. account is required on create_media_buy and update_media_buy, and optional on the reads.

Brand identity is a domain

AdCP identifies advertisers by domain, not by company name. Dappier resolves the display name and description from the domain’s /.well-known/brand.json where available, falling back to your brief and then to a name derived from the domain.

ID prefixes

Context passthrough

create_media_buy, update_media_buy, and the read tools accept a context: Record<string, unknown> field, echoed back unchanged on the response — useful for correlating tool calls with your own session state. Packages carry their own context too, so you can map Dappier’s package_id onto your own line item.

Error shape

Errors follow AdCP 3.1:
Every error carries a recovery hint — the field that tells a buying agent whether to retry:

Error code reference

Field paths in errors are translated into your request’s vocabulary — you get packages[0].creatives[0].assets.cta_url, not the backend’s internal campaign.cta_link.

Typical campaign lifecycle


Tools Reference

get_adcp_capabilities

Purpose: The first call a buyer should make. Tells you which AdCP versions Dappier speaks, which protocols it implements, the auth and billing model, idempotency semantics, and per-protocol feature flags. Network behavior: In-memory lookup. No outbound call. Returns instantly.

Inputs (all optional)

Success response

inline_creative_management: true is what makes packages[].creatives a legal input. has_creative_library: false is what makes it the only way to supply a creative — there is no sync_creatives step and no reuse by creative_id.

get_products

Purpose: Discover what Dappier sells. Call this before create_media_buy — it returns the product_id and pricing_option_id a media buy requires. Network behavior: In-memory. No API call.

Two buying modes

refine mode is not supported — this agent publishes a single product and issues no proposals.

Inputs

Filters never error. A filter this product cannot satisfy returns an empty products array, not a failure — so you can fan out across many sellers and read the empty result as “no match here”.

Success response

brief_relevance is present only in brief mode. available_metrics is a binding contract — spend is deliberately absent because Sponsored Conversations are flat-rate with no per-impression rate to meter.

list_creative_formats

Purpose: Get the full asset specification for Dappier’s creative format. Call this before create_media_buy — it tells you exactly which assets a brand agent needs. Network behavior: In-memory. Instant. Dappier publishes one format, dappier_brand_agent. It is not a banner or a video: it is a conversational brand agent, so its assets are a persona, knowledge sources, promoted questions, a call-to-action, and widget branding.

Inputs (all optional)

The dappier_brand_agent asset spec

Individual assets — supplied by asset_id: Repeatable groups — flattened to zero-indexed keys of the form {group}_{index}_{asset}, not nested arrays:
Indices must start at 0 and be contiguous, and all keys for one index must be supplied together. min_count and max_count are enforced — fewer than 3 sponsored prompts or fewer than 6 follow-ups is rejected.
Asset values are always objects, never bare strings:

create_media_buy

Purpose: Create a Sponsored Conversations campaign. One call creates the brand agent, the campaign, and the branded chat widget together. There is no separate creative-upload step.
The result is not live. The campaign is registered paused pending Dappier trafficking and reviewer activation, and confirmed_at comes back null because Dappier has not yet committed to the buy. Report it as registered and pending activation — never as live or serving.

Top-level inputs

start_time and end_time are independent — you can set an end without a start, or the reverse. Never invent a flight window to fill a field; the response message states the actual schedule, so repeat that back.

Package inputs

Include the category, use cases, and adjacent topics in keyword_targets — not just brand names. Someone searching the brand by name is already converted.

Success response

Three response fields are worth relaying to the advertiser verbatim:
  • demo_url — a working, shareable page rendering the brand agent exactly as an end user will see it, with this buy’s placement selected. It works immediately even though the campaign is paused. Never construct or guess this URL.
  • distribution — the two activation snippets. See Distributing the Brand Agent.
  • platform_links — deep links into the advertiser’s own agent on the Dappier platform, with the click path for each, so they can upload or replace the logo, banner, and thumbnail themselves. These are environment-specific; use the URLs from the response rather than hand-assembling them.
On an idempotent replay the response carries replayed: true. Omission means it was freshly executed.

Not supported

Sending any of these returns an explicit error rather than being silently ignored: proposal_id, total_budget, invoice_recipient, reporting_webhook, plan_id, and at the package level pacing, bid_price, impressions, optimization_goals, creative_assignments, catalogs, measurement_terms, performance_standards, committed_metrics, and all geo / device / language / audience targeting.

Example


get_media_buys

Purpose: Read the current state of campaigns — status, budget, the SmartSync pixel, and the revision token you need before any update. Use this for “what’s the current state of my campaigns?”. For performance, use get_media_buy_delivery.

Inputs (all optional)

Dappier only ever reports active and paused. The other AdCP statuses are accepted as filters but return an empty page. A campaign whose flight has ended still reports active — do not poll this expecting to observe completion.

Success response

Status lives on a different field here. This tool returns lifecycle state on media_buys[].status; create_media_buy and update_media_buy return the same value on media_buy_status. Same vocabulary, two field names.

Valid actions by status

cancel and update_dates are deliberately absent: Dappier does not cancel over the API, and a Sponsored Conversations campaign is not date-scheduled.

Not supported

include_snapshot, include_history, include_webhook_activity.

update_media_buy

Purpose: Change an existing campaign — pause or resume it, adjust the budget, replace its contextual keywords, or revise the prompts and image assets. PATCH semantics. Only the fields you send change; anything omitted is left exactly as it was.

Inputs

Pausing, resuming, and activation

paused: false is how a campaign created paused gets activated — once the SmartSync pixel is trafficked in the advertiser’s DSP, which is what makes the buy deliver. The first activation is when Dappier commits to the buy, so confirmed_at is stamped then. Resuming a campaign Dappier has not yet trafficked returns INVALID_STATE.

Revisable assets

Send the same asset keys you used on create, under packages[0].creatives[0].assets: Edits reach the live widget in the same call, and a prompt whose text is unchanged keeps its id, so its reporting history is continuous.
Any other asset key — persona, cta_url, cta_button_text, primary_color, welcome_*, knowledge_source_* — is rejected by name. The rest of the brand agent is immutable after creation.

What cannot be changed

Success response

Examples

Activate a campaign:
Add a banner you couldn’t source at create time:
Replace the sponsored prompts:
Prompts not listed in a group you send are removed. Send the full desired list, not just the additions.

get_media_buy_delivery

Purpose: Retrieve delivery performance — impressions, clicks, click-through rate, and call-to-action engagements, reported per package. Use this for “how are my campaigns performing?”. For current state, use get_media_buys.

Inputs (all optional)

Lifetime only

There is no date-range reporting. Sending start_date or end_date returns DATE_RANGE_NOT_SUPPORTED. Every response covers campaign start through as_of. reporting_dimensions, time_granularity, and include_window_breakdown are likewise unavailable.

What the metrics mean

No spend figure. Sponsored Conversations are sold flat-rate with no per-impression rate, so spend-to-date is not something Dappier can compute and is not reported. currency denominates the campaign budget, not spend. Every row carries is_final: false — these numbers are for pacing and reporting, never for invoicing.

Success response

Reading zeros

impressions: 0 means no delivery yet, not missing data — an absent row means no such campaign. Check as_of and by_package[0].delivery_status before concluding a campaign is underdelivering. A campaign live for less than the reporting lag reports no delivery_status at all rather than guessing.

provide_performance_feedback

Purpose: Tell Dappier how a campaign actually performed for you, so delivery can be tuned over time. This is the reverse of get_media_buy_delivery. That tool reports what Dappier served; this one reports what that traffic was worth, measured by your own attribution. Dappier cannot see whether a prompt click became a subscription — only you can.

Inputs

performance_index is a normalized score, not a count. Normalizing is the point: you signal quality without disclosing revenue, conversion volume, or margin. Do not send a conversion count, a currency amount, or a percentage.
What Dappier does with it today: the signal is stored, not acted on. It will not change how the campaign runs right now — it is captured so it can inform tuning once optimization exists. Do not report to an advertiser that delivery has been adjusted.
Send feedback after a measurement period closes and your attribution has settled — typically weekly or monthly, with a fresh idempotency_key per period. Re-scoring an earlier period after re-running attribution is fine and expected: send it as new feedback with its own key, and both readings are retained.

Example


Distributing the Brand Agent

create_media_buy returns a distribution object: one Brand Agent, two independent distribution channels. They do not depend on each other, and both can run at once. Present them to the advertiser as two separate options — never merge them, and never report one as the only way to activate.

1. Activate across Dappier AskAI

Sponsored Prompt → user engages → Brand Agent opens inside AskAI
Distributes the Brand Agent through Sponsored Prompts inside Dappier AskAI publisher inventory. Activation method: the SmartSync Pixel.
Add this pixel to the companion creative or campaign configuration in your DSP, ad server, or buy-side platform. Dappier uses the External Campaign ID to map the programmatic campaign to its Sponsored Prompts.
The SmartSync pixel activates this Brand Agent across Dappier AskAI inventory only. It is not what runs the agent on the open web.
The same smart_pixel object is returned at the top level of create_media_buy and on every get_media_buys row.

2. Run as a Standalone Brand Agent

300x600 programmatic impression on the open web → the same Brand Agent
The same Brand Agent, trafficked as a standalone interactive ad across open-web inventory through a DSP, ad server, or compatible programmatic platform. Activation method: a JavaScript Creative.
Both lines are required — the loader script tag and the widget element. The loader reads its bootstrap config only from its own script tag, so widget-id there is not redundant with the element’s widgetId.
Use the snippet exactly as returned in distribution — the Widget ID and Placement ID are already filled in for the buy. Do not replace them, and do not hand-assemble the tag.

Macros

creativeId, lineitemId, publisherId, and clickUrl are DSP / ad-server macros, substituted by the serving platform at impression time. Map each one to the equivalent macro your platform supports — any macro left unmapped is delivered verbatim as a literal %macro_...% string.
If distribution.omitted is present, the standalone option could not be built for that buy (no placement id was returned). Report it as unavailable rather than assembling a tag by hand.

End-to-End Recipes

Launch a campaign (minimum viable flow)

What’s happening with my campaigns right now?

Or for specific campaigns, regardless of status:

How is a paused campaign performing?

The active-only default applies to delivery even when you name ids, so pass the status filter too:

Add an image the advertiser supplied after launch

Or point the advertiser at the matching platform_links entry — they can upload it themselves without another API call.

Broaden the keywords

Keywords replace, they don’t merge. Send the full set:

Stop a campaign

There is no cancel. paused: true is how delivery ends:

Guardrails the Server Enforces

These are quiet-but-strict rules callers often trip over.
On create_media_buy, never infer cta_url or cta_button_text from the brand — ask the advertiser. The same goes for flight dates and budget: omit them rather than inventing values, and report back what the response message actually says.

FAQ

They were removed. Creative creation is folded into create_media_buy — one call creates the brand agent, the campaign, and the widget together — and creative revision into update_media_buy. This agent declares inline_creative_management: true and has_creative_library: false, so creatives are package-scoped and never reusable by creative_id.
No. Call it once per session to discover the surface, then cache the result.
Every campaign is registered paused with confirmed_at: null, pending Dappier trafficking and reviewer activation. Once the SmartSync pixel is trafficked in your DSP, update_media_buy { paused: false } activates it — and that first activation is when confirmed_at is stamped. Resuming before Dappier has trafficked it returns INVALID_STATE.
That happens when a retry mints a fresh idempotency_key. Reuse the same key — the response then carries replayed: true and nothing is created twice. If you get IDEMPOTENCY_EXPIRED, check whether the original call succeeded (call get_media_buys and match on the context you sent) before minting a new key.
Yes — packages[0].budget, as a plain number. It is optional: Sponsored Conversations are flat-rate, agreed offline with Dappier, so omit it rather than guessing a figure. The buy is registered either way.
No. Contextual keywords are the only targeting signal. Every campaign runs across the full Dappier network. Sending geo, device, language, or audience targeting returns an explicit error rather than being silently dropped.
Set end_time at creation if the advertiser named one. After creation, flight dates are immutable — paused: true is the end date.
Sponsored Conversations are sold flat-rate with no per-impression rate stored, so spend-to-date is not a number Dappier can compute. spend is deliberately absent from the product’s available_metrics rather than reported as a fabricated value. currency denominates the campaign budget only, and every row carries is_final: false.
Fields AdCP defines but Dappier doesn’t honor — pacing, bid_price, optimization_goals, reporting_webhook, include_snapshot, reporting_dimensions, and so on — are rejected with UNSUPPORTED_FEATURE and the exact field path, not silently ignored. An advertiser who thinks they bought US-only inventory needs to be told they did not.
Pass context: { your_request_id: "..." } on the buy, and/or packages[0].context on the package — both are echoed back unchanged, and the package one comes back on get_media_buys too.
Treat it as opaque — round-trip the pagination.cursor value verbatim and follow it while pagination.has_more is true. Don’t parse or construct it yourself.
No. One media buy = one campaign = one package = one Brand Agent. Create separate media buys for separate campaigns.
Not today. The signal is stored so it can inform tuning once optimization exists. Don’t report to an advertiser that delivery has been adjusted.

Glossary


Conclusion

The Dappier Sales Agent MCP gives AI agents a complete, AI-callable surface over the Dappier Sponsored Conversations network — a brand agent, campaign, and branded widget in a single call, two ready-to-traffic distribution channels, delivery reporting, and a feedback loop — while keeping pricing and activation with Dappier. 🔗 Explore further: