Waverunner MCP server
Let any MCP-capable agent (Claude, Cursor, ChatGPT, custom agents) run your self-serve always-on advertising: add businesses, create and launch campaigns, and read performance.
Quick start (two minutes)
- Create an API key: in the app, go to Settings → API keys, create a key, and copy it (it's shown once). Keys are scoped to the organization that was active when you created them; switch workspaces in the sidebar first if you need a key for another organization.
- Add the server to your client: point it at
https://waverunner.adwave.com/api/mcpwith the key as a bearer token (per-client configs below). - Ask your agent something: try "List my Waverunner campaigns and their spend". The agent discovers the tools automatically.
How to connect each client
Cursor: add to .cursor/mcp.json (or Settings → MCP). Any client that supports Streamable HTTP with custom headers uses this same shape:
{
"mcpServers": {
"waverunner": {
"url": "https://waverunner.adwave.com/api/mcp",
"headers": { "Authorization": "Bearer wr_YOUR_KEY" }
}
}
}Claude Code: one CLI command:
claude mcp add --transport http waverunner https://waverunner.adwave.com/api/mcp \ --header "Authorization: Bearer wr_YOUR_KEY"
Claude Desktop and other stdio-only clients: bridge with mcp-remote in claude_desktop_config.json:
{
"mcpServers": {
"waverunner": {
"command": "npx",
"args": [
"-y", "mcp-remote", "https://waverunner.adwave.com/api/mcp",
"--header", "Authorization: Bearer wr_YOUR_KEY"
]
}
}
}Custom agents: the official MCP TypeScript SDK (Python and other SDKs work the same way):
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const client = new Client({ name: "my-agent", version: "1.0.0" });
await client.connect(
new StreamableHTTPClientTransport(new URL("https://waverunner.adwave.com/api/mcp"), {
requestInit: {
headers: { Authorization: `Bearer ${process.env.WAVERUNNER_API_KEY}` },
},
}),
);
const { tools } = await client.listTools();
const report = await client.callTool({
name: "get_campaign_report",
arguments: { campaignId: "018f..." },
});The server is stateless: no sessions to create or clean up, and a dropped connection can simply reconnect. Treat the API key like a password. Anyone holding it can spend from your wallet.
Tools
READlist_businesses
List your businesses with their analysis status. Poll after add_business until ready.
WRITEadd_business
Add a business by website URL and kick off analysis (scrape, profile, personas). Free.
WRITEarchive_business
Archive a business that has no live or launching campaigns. Hidden from list_businesses; restore any time with unarchive_business.
WRITEunarchive_business
Restore an archived business to active use.
READget_business
Business detail: the extracted profile and every persona on the business profile with its id and Persona Reach (targetingSeeds). Campaigns copy Reach after interest targeting is planned; persona seeds stay as written on the Creative profile.
READlist_creatives
List generated creatives for the business (newest first) with static variants, video ads, render status, and asset URLs. Paginated: default 20, max 100 per page; pass the returned nextCursor to fetch the next page.
WRITEcreate_persona
Create a persona for a business from free-text notes (enqueues enrichment; the worker builds the full persona). Free.
WRITEupdate_persona
Partial update of a persona: name, description, demographics, motivations, pain points, selected flag, and/or demographic pins and exclusions (catalog segment keys).
WRITEgenerate_creatives
Charge and enqueue a library ad generation batch for a ready business (paid first from creation credit, then the wallet). Quote first with get_creation_quote.
READlist_campaigns
List campaigns with objective, status, and daily budget.
READget_creation_quote
Pre-flight price for an ad generation batch. Pass campaignId for statics × interest lines; omit for library Launch-pack pricing.
WRITEcreate_campaign
Create a draft campaign, optionally with targeted states, named platforms (Google, Meta, …), a landing URL, a geo-holdout for lift measurement, and audience boost. After interest targeting is planned, Autopilot builds a recommended creative plan (reuse ready creatives, generate only gaps across targeted personas; generationDeferred). Pass campaignAds: false for reuse-only. Metered: quote with campaignId once lines exist. Channels use Preparing → Starting → Live after launch.
READget_campaign
Full setup detail for one campaign: status, budget, flight dates, landing URL, targeted states, platforms, audience boost, geo-holdout states, personas, and the offer/instructions brief.
WRITEupdate_campaign_budget
Change a campaign's daily budget. No proration; the next daily charge uses the new value.
WRITErename_campaign
Rename a campaign in any status except mid-launch. Display-only; never affects serving or billing.
WRITEupdate_campaign_end_date
Set or clear the flight end date on a live or paused campaign (YYYY-MM-DD or null).
WRITEupdate_campaign_landing_url
Change the landing page on a live or paused campaign; refreshes serving ads to the new URL.
WRITEupdate_campaign_setup
Edit draft campaign setup: US states (empty = national), named platforms, geo-holdout (measureLift), and/or audience boost. Refused once the campaign has launched. Send at least one field.
WRITEset_audience_boost
Turn audience boost on or off (draft, live, or paused). Turning it on requires an imported customer list for that business.
WRITElaunch_campaign
Launch a draft. Charges day one when ads are ready, or commits with awaitingCreatives while ads are still rendering.
WRITEcancel_campaign_launch
Cancel a launch that is still waiting on creatives (no charge yet). Returns the campaign to draft.
WRITEretry_campaign_launch
Retry a failed campaign: resets it to draft and re-runs the launch gate (may defer charge while ads render).
WRITEretry_failed_placements
Re-run distribution for a live campaign's failed placements only; serving placements are left untouched.
WRITEpause_campaign
Pause a live campaign. Future daily charges stop; today's charge stands.
WRITEresume_campaign
Resume a paused campaign. Re-charges today idempotently before serving restarts.
WRITEend_campaign
Permanently end a campaign. Stops all serving and charges; cannot be undone.
WRITEarchive_campaign
Archive a campaign that is not launching or live. Hidden from list_campaigns; restore any time with unarchive_campaign.
WRITEunarchive_campaign
Restore an archived campaign to your lists.
WRITEdelete_campaign
Permanently delete a never-run draft campaign. Generated ads stay in the Ad Library; campaigns that have run can only be archived.
READget_campaign_report
Full performance rollup: spend, conversions, revenue, CPA, ROAS, sessions (ad visits from this campaign's ads), per-platform breakdowns with honest channel fields (clickable vs TV views, assisted conversions as reporting-only union plus path breakdown, identity coverage, seed impact), and CPM breakout (all-in, audience list-rate fees, media remainder).
READget_campaign_analytics
Daily funnel timeseries (impressions → clicks → ad visits/sessions → conversions), device/region mix (observed phones/computers/TVs, not sold channel cards), where ads appeared as publishers (TV channels, web/apps, Google networks, Meta platforms), per-persona and per-segment performance, per-day spend story. Channel cards stay Mobile web / TV / Google / Meta; deviceMix is a breakdown.
READget_campaign_lift
Lift scorecard: geo-holdout causal lift, cross-channel overlap, and Autopilot effectiveness (acted vs observe-only), each with a 95% CI and significance flag. Includes holdoutInconclusive when the holdout read is still directional.
READget_campaign_customers
New vs returning split of a campaign's attributed conversions, with cost per new customer.
READget_business_ltv
Customer lifetime value for a business: average/quartile LTV and acquisition-month payback cohorts.
READlist_recommendations
Pending cross-campaign budget moves the portfolio optimizer computed but didn't auto-apply, with the marginal-CPA evidence.
WRITEapply_recommendation
Apply a pending budget move between two campaigns (the same validated transactional path as a manual budget edit).
WRITEdismiss_recommendation
Dismiss a pending budget recommendation without moving any money.
READget_campaign_ads
Every ad in the campaign Ads set (not the business creative library) with status, persona, and lifetime performance.
WRITEset_ad_serving
Pause or resume one campaign ad across every platform. On live or paused campaigns, pass reasonCode (Autopilot override). Use resume_optimizer_pause for Autopilot pauses and resume_user_pause for manual pauses. Refuses to pause the last serving ad on this interest segment line.
READlist_audiences
Website audiences (observe-first): first-party visitor segments with member counts, platform audiences (campaign reach, synced lists, lookalikes), customer sources for Audience boost, plus Customize targeting guides (interest and demographic catalog picks Autopilot always includes on the next launch). Persona Reach (targetingSeeds) is on each persona via get_business.
READlist_integrations
List connected customer-data sources (provider, status, last sync). Tokens are never returned. Pass businessId to filter to one website.
WRITEsync_integration
Enqueue a sync for an already-connected customer-data source. Connect/disconnect stay in the web app.
WRITEupdate_organization
Update organization product settings. Currently just crossCampaignOptimization: enabling it lets the portfolio optimizer apply cross-campaign budget moves automatically.
READexport_org_data
Full organization data export as one large JSON document (businesses, personas, creatives, campaigns, ledger entries, delivered spend, segments, per-ad performance, and more). Rate limited to 5 requests per hour per organization.
WRITEcreate_segment
Create a custom first-party segment for a business with a rule definition, evaluated immediately.
WRITEset_segment_active
Activate or pause a segment. Paused segments stop evaluating membership and syncing.
WRITEdelete_segment
Delete a custom (non-system, non-discovered) segment.
WRITErefresh_segment
Enqueue a re-evaluation of one segment's membership.
WRITEcustomize_audience
Add or remove an interest or demographic catalog pick that Autopilot always includes on the business's next launch.
WRITEresolve_audience_suggestion
Accept or dismiss a discovered-audience suggestion. Accept creates a segment with the suggestion's exact proposed rule.
WRITEupdate_audience_sync_consent
Set organization-wide consent for syncing customer lists to ad platforms.
WRITEimport_customer_csv
Import a customer CSV for a business to seed Audience boost. Hashed at rest; raw emails are never returned. Rate limited.
READget_tracking_setup
A business's site tag snippet, server-side conversion webhook (one per website), conversion rules, and settings. Wire your own systems into attribution.
WRITEcreate_conversion_rule
Create a URL-pattern conversion rule for a business. Intent-page-looking patterns require acknowledgeIntentPage: true.
WRITEupdate_conversion_rule
Set or clear a conversion rule's per-conversion value estimate. Applies to new conversions only.
WRITEdelete_conversion_rule
Delete a conversion rule.
WRITEupdate_tracking_settings
Update a business's tracking-tag settings (autocapture, lead detection, origin enforcement, view-through window). Disabling recommended settings requires acknowledgeImpact: true.
WRITEupsert_form_conversion_value
Set or clear the per-conversion value estimate for a named Form conversion (lead, signup, or subscribe). Creates a value-only carrier when needed; clearing with null removes the estimate. Applies to new conversions only.
WRITEresolve_conversion_suggestion
Accept or dismiss an AI-discovered conversion suggestion. Accept creates or links a conversion rule; dismiss remembers the verdict forever.
WRITEsend_test_conversion
Record a test conversion event for a business to verify the tag/webhook pipeline end to end.
READget_wallet
Prepaid wallet balance and auto-refill state. Check before launching.
READget_wallet_ledger
Paginated wallet ledger (newest first): deposits, daily campaign charges, refunds, adjustments, and ad-creation charges. Default 25, max 100 per page.
READlist_catalogs
List Catalog Video product catalogs (requires Catalog Video early access).
WRITEcreate_catalog
Create a Catalog Video product catalog from a shop URL, feed URL, or csvText (CSV). Requires Catalog Video early access.
READget_catalog_items
List synced products for a catalog (SKU, title, price, image). Paginated (limit/cursor, default 100, max 500).
WRITEsync_catalog
Enqueue a catalog re-sync from its source.
READlist_catalog_video_runs
List Catalog Video production runs for a business. Paginated (limit/cursor, default 50, max 200).
WRITEcreate_catalog_video_from_url
Kick off the shop-URL → Catalog Video auto-pipeline for a business. Idempotent on (businessId, url). Requires Catalog Video early access.
WRITEcreate_catalog_video_run
Create a draft Catalog Video run linked to a catalog (optional template + SKUs + defaultTier). Requires Catalog Video early access.
READget_catalog_video_run
Catalog Video run detail with progress counters and status.
READquote_catalog_video_run
Pre-flight price for a Catalog Video run (sample or full tranche).
WRITElaunch_catalog_video_run
Charge the first tranche and start Catalog Video production. Pass the quote's mode + quoteHash to guard against a stale quote. Metered.
WRITEupdate_catalog_video_run
Edit SKUs, aspect ratios, tier, sync cadence, refresh budget, new-item policy, or launch mode while draft/quoting/paused.
WRITEpause_catalog_video_run
Manually pause a producing/live Catalog Video run. Money-neutral.
WRITEresume_catalog_video_run
Resume a paused Catalog Video run and re-arm remaining production work.
WRITEarchive_catalog_video_run
Archive a draft/failed/paused/live Catalog Video run. Terminal, non-refunding.
WRITEretry_catalog_video_run
Clear the error on a failed Catalog Video run and resume it.
READget_catalog_video_manifest
Per-variant CDN / VAST URLs for a Catalog Video run. Paginated (limit/cursor, default 100, max 500).
READget_catalog_video_feeds
Platform supplemental feed URLs once a run is live.
READexport_catalog_video_run
Google video_link activation kit (instructions + CSV) for operator handoff.
READlist_catalog_video_templates
List Catalog Video templates for the org (optional businessId).
READget_catalog_video_template
Catalog Video template detail: status, hook count, pitched concept.
WRITEpitch_catalog_video_template
Pitch a Catalog Video template (optional hookCount for opening-hook variants) and start the human-review probe pipeline.
WRITEapprove_catalog_video_template
Human-approve a Catalog Video template before launching a run.
READlist_campaign_catalog_video_runs
List bindable Catalog Video runs for a campaign and its current binding. Requires Catalog Video early access.
WRITEbind_campaign_catalog_video
Bind or clear a campaign's catalogVideoRunId for native Catalog Video serving (requires Catalog Video early access).
The full workflow, step by step
This is the sequence agents are taught via the server's instructions; each step maps to one tool call:
add_businesswith your website URL. Analysis is free and takes a minute or two. Polllist_businessesuntil the status isready.get_business: review the extracted profile and the personas. Each persona has an id and Persona Reach (targetingSeeds); pass a subset asicpIdsto target specific personas. After interest targeting is planned, Reach is copied into that campaign's flight targeting.create_campaign: a draft; launching is separate. After interest targeting is planned, Autopilot builds a recommended creative plan (reuse ready creatives, generate only gaps across targeted personas;generationDeferred: true; metered: creation credit first, then the wallet). PasscampaignAds: falsefor a reuse-only plan with no generation charge.get_creation_quote: price the batch. PasscampaignIdonce lines exist for the recommended creative plan gaps (statics only where needed across personas, plus missing videos); omitcampaignIdfor library Launch-pack pricing. Returns unit breakdown, billing mode, and remaining creation credit. Watch generation withlist_creatives.get_wallet: confirm the balance covers at least the first day, thenlaunch_campaign.get_campaign_report: spend, conversions, revenue, CPA, ROAS, per-platform breakdowns. Manage spend withupdate_campaign_budget,update_campaign_end_date,update_campaign_landing_url,pause_campaign,resume_campaign,end_campaign.- Optimize:
get_campaign_analyticsfor daily trends and persona/segment/device breakdowns,get_campaign_adsfor per-ad numbers, thenset_ad_servingto pause the weak ads (on live or paused campaigns, pass areasonCode; when resuming, useresume_optimizer_pauseorresume_user_pauseto match how the ad was paused). Wire your own conversion data in withget_tracking_setupso the loop runs on real outcomes.
A tool call and its result look like this (all money values are integer cents):
// → tools/call
{ "name": "create_campaign",
"arguments": {
"businessId": "018f3a2b-...",
"name": "Summer Sale",
"objective": "sales",
"dailyBudgetCents": 5000,
"offer": "Free shipping over $40"
} }
// ← result
{ "campaign": { "id": "018f3c91-...", "status": "draft" },
"campaignAdsEnqueued": false, "generationDeferred": true }Use cases & example prompts
Paste these into any connected agent and adjust the specifics. They exercise the whole surface:
Zero to live campaign
You have a website and a budget, and want an agent to handle everything: analysis, personas, ad generation, and launch.
Add acme-coffee.com to Waverunner, wait for the analysis, show me the personas it found, then create a $50/day sales campaign with the offer “free shipping over $40”. Show me the generated ads and the wallet balance, then ask me before launching.
Morning performance check
A recurring assistant task: pull yesterday's numbers and flag anything that needs a decision.
Pull the report for every live Waverunner campaign. Summarize spend, conversions, CPA, and ROAS, compare platforms, and tell me if anything looks off or is worth pausing.
Budget management
Shift money toward what works without logging into the dashboard.
My “Summer Sale” campaign is converting at 4x ROAS and “Brand Awareness” is barely spending. Raise Summer Sale to $80/day and drop Brand Awareness to $15/day.
Spend guardrails
Stop spending fast when priorities change: pausing keeps the campaign resumable, ending is permanent.
We're out of inventory until Thursday. Pause every live Waverunner campaign now, and remind me to resume them Thursday morning.
Audience review
Review Website audiences and platform sync state before planning the next campaign.
List my Waverunner audiences. How many people are in the high-intent segment, and which platform audiences are synced and ready for Delivery?
Closed-loop creative optimization
The full optimization loop: pull daily and per-ad data, join it with your own numbers (margins, inventory, CRM), decide, and push the changes back.
Pull the last 30 days of analytics and the per-ad performance for my “Summer Sale” Waverunner campaign. Cross-reference the conversion revenue with the product margins in my spreadsheet, then pause any ad whose margin-adjusted CPA is above $40 and tell me if the budget should move.
Wire in your own conversion data
Attribution is only as good as the conversions it sees. Feed them from any backend so reports and per-ad numbers reflect reality.
Get my Waverunner tracking setup and write a small script that posts a conversion to the webhook whenever a Stripe payment succeeds, including the order id and the customer email.
Errors & limits
Failed tool calls return the HTTP status and the same JSON error the REST API sends, so agents can react precisely:
| Error | Meaning | What to do |
|---|---|---|
| 401 unauthorized | Missing, invalid, or revoked API key | Create a new key in Settings and update the client config |
| 402 insufficient_funds | The wallet can't cover the first day's budget | Top up in the app (Wallet), then launch again |
| 404 *_not_found | The id doesn't exist in your organization | Re-list (businesses/campaigns) and use a returned id |
| 409 no_servable_ads | No creative plan and a targeted persona still has no finished ad | Accept a creative plan (or wait for renders), then launch — launch can commit while ads are still rendering when a plan exists |
| 409 invalid_status | The campaign isn't in a state that allows the action | Check list_campaigns (e.g. only live campaigns pause) |
| 409 last_serving_ad | Pausing this ad would leave its persona with nothing to serve | Resume or generate another ad for that persona first |
| 400 override_required | Live or paused campaign: Autopilot manages this creative; reasonCode is required | Pass reasonCode (and optional note) on set_ad_serving, then retry |
| 400 resume_reason_mismatch | Resume used the wrong dedicated reason for how the ad was paused | Use resume_optimizer_pause for Autopilot pauses, resume_user_pause for manual pauses |
| 422 invalid_input | A parameter failed validation | Fix the field named in the issues array and retry |
| 429 rate_limited | Too many requests this minute | Back off and retry (120 reads / 30 writes per minute) |
Rate limits are shared with the REST API: 120 reads and 30 writes per minute per key, plus 120 MCP requests per minute. Analysis and ad generation are subject to the daily fair-use caps on the pricing page.
Money safety
launch_campaign and resume_campaignmove media money: launching charges the first day's budget from your prepaid wallet immediately (and the daily budget each active day after); resuming a paused campaign re-charges the current day. Ad generation (inside create_campaign) is metered per unit, priced up front by get_creation_quote, drawn from creation credit before the wallet, and rejected with a 402 (nothing created) when neither covers it. Adding businesses and analysis are always free, and a launch fails cleanly (never overdrafts) when the wallet can't cover day one. Agents are told to confirm with you before launching, resuming, or ending, and money only ever leaves the prepaid wallet you topped up, never a card on file (unless you opted into auto-refill).