affset MCP server
This server provides comprehensive media-buying management for an affset tenant through chat:
Read/Reporting: Retrieve tenant info, traffic statistics (grouped by various dimensions), list campaigns, zones, team, payout rules, targeting rules, sub labels, and conversion audit records. Generate zone URLs and direct tracking links.
Campaigns: Create, update, and run/pause campaigns; set name, offer URL, budgets, pacing, etc.
Zones: Create and update zones; blacklist underperforming zones (cut_zones) based on CR, spend, or ROI.
Payouts: Manage campaign payout rules (global and zone-specific) and set payout goals.
Targeting: Set/remove rules for geo, device type, OS, browser, zone blacklist, and frequency capping.
Team & Sub Labels: Invite team members and manage sub1–sub5 display names.
Safety: All mutations are dry-run by default (confirm required); optional read-only mode prevents any changes.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@affset MCP servershow me stats for campaign 'Summer Sale' grouped by zone"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
affset MCP server
An MCP server that lets a media buyer run affset from a chat client — pull stats, manage campaigns/zones/team, payouts, targeting, sub labels, and cut underperforming zones in plain language, no dashboard.
It's a thin wrapper over the existing affset tenant API (Bearer token +
X-Namespace). One server instance serves one tenant.
Tools
Tool | What it does |
| Show the tenant this server is bound to: namespace, API base, derived dashboard URL, and (when readable) company / timezone / custom API domain. Read-only. |
| Traffic stats grouped by a dimension (date, campaign, zone, country, sub1–5, …). Returns clicks, conversions, CR, payout, media cost and ROI as a table. Sub columns use the tenant's sub labels when configured. |
| List campaigns (status / name filter, pagination). |
| List traffic-source zones (status / name filter, pagination). |
| List team members (email, role, manager). Never returns API tokens. |
| Invite a team member (owner, manager, publisher, advertiser, publisher_manager, advertiser_manager). A scoped manager key can only create its own managed role, self-assigned. Returns the new API key once — |
| Create a campaign from an advertiser email, offer URL, geo, payout and name. Defaults: CPA / rate 0, paused, global payout rule, ready tracking link with |
| Run or pause a campaign ( |
| Partial update (name, offer URL, status, rate, budgets, dates, …). Dry-run by default; |
| Create a traffic-source zone (name + optional postback/site/traffic-back URLs). Always created |
| Partial update (name, status, URLs). Dry-run by default; |
| The |
| The |
| Blacklist underperforming zones on a campaign by threshold (CR / spend / ROI). Dry-run by default; |
| List a campaign's global + per-zone payout rules and its |
| Upsert a global or zone-specific payout. Dry-run by default; |
| Delete a global or zone-specific payout rule. Dry-run by default; |
| Set or clear |
| Catalog of targeting rule types, flagging the seeded ones |
| List a campaign's targeting rules, flagging any that have no effect. |
| Upsert one targeting rule (safe merge), normalised to what |
| Remove one targeting rule by id or type+method. Dry-run by default; |
| List tenant display names for sub1–sub5. |
| Set or clear sub labels (partial; |
| List conversion audit records (payout, spend, pixel type, payload, postback). Optional client-side filters on the current page. |
Which URL do I give the network?
|
| |
Picks the campaign | affset, from the zone's rotation | you, one fixed campaign |
Needs an active campaign | yes — otherwise traffic back / unsold | yes — otherwise 404 |
Needs an active zone | yes | yes |
Geo & targeting rules | enforced | not enforced |
| the impression row | the click row |
Use one or the other for a given traffic stream — never both with cost=, or the
media cost is counted twice.
Both use the tenant's custom API domain when one is set, since the URL gets pasted
into the network verbatim. Macros ({clickid}, [CLICK_ID], ${SUBID}) are inserted
without percent-encoding — the source expands them before the request reaches affset.
cut_zones only ever adds zones to a campaign's blacklist, and does a
read-merge-write so existing targeting rules are never touched.
create_campaign needs a traffic-source zone for the tracking link: pass
zone_id, or let it auto-pick when the namespace has exactly one active zone.
Campaigns are created paused; activate them before sending traffic through either
URL. Both URL types also require an active zone. Geo whitelist is enforced in /serve
only — the direct tracking link is not geo-gated, but it still requires an active,
currently serviceable campaign.
Related MCP server: PropellerAds MCP Server
Configuration
All config comes from environment variables (never hard-coded):
Variable | Description |
| Origin of the affset API, e.g. |
| Tenant API key. Its namespace must match |
| Tenant namespace (lowercase letters, numbers, hyphens; 3–63 chars — same rules as signup). |
| Optional, default |
| Optional, default |
See .env.example.
Install
From npm (recommended)
No clone, no build — your MCP client runs it with npx. For Claude Desktop
(claude_desktop_config.json):
{
"mcpServers": {
"affset": {
"command": "npx",
"args": ["-y", "@affset/mcp"],
"env": {
"AFFSET_BASE_URL": "https://api.affset.com",
"AFFSET_API_KEY": "sk_live_...",
"AFFSET_NAMESPACE": "your-namespace"
}
}
}
}For Claude Code:
claude mcp add affset \
-e AFFSET_BASE_URL=https://api.affset.com \
-e AFFSET_API_KEY=sk_live_... \
-e AFFSET_NAMESPACE=your-namespace \
-- npx -y @affset/mcpSame env flags with -- npx -y github:affset/mcp if you install from GitHub
instead of the npm registry (see below).
Add -e AFFSET_READ_ONLY=true for a stats/reporting-only instance (see
Security).
From GitHub directly (no npm publish required)
npx can install straight from the git repo instead of the npm registry —
useful if you'd rather not publish, or just want to track main without a
release step:
{
"mcpServers": {
"affset": {
"command": "npx",
"args": ["-y", "github:affset/mcp"],
"env": {
"AFFSET_BASE_URL": "https://api.affset.com",
"AFFSET_API_KEY": "sk_live_...",
"AFFSET_NAMESPACE": "your-namespace"
}
}
}
}A push to main makes that commit available to this unpinned install path — no
npm publish is required. On resolution, npm fetches the repository and runs the
prepare script to build dist/ before starting the binary. npm may reuse its
cache on later starts; an already running MCP process is not updated until it is
restarted and npx resolves the dependency again.
For reproducible deployments, pin a reviewed ref instead of floating on main:
github:affset/mcp#<commit-sha> or github:affset/mcp#<tag>. Restart the MCP
process deliberately when you want it to resolve and run a newer revision.
From source
git clone https://github.com/affset/mcp.git affset-mcp
cd affset-mcp
npm install # builds via the prepare scriptThen point your MCP client at the built entry file — swap the npx command above
for "command": "node", "args": ["/absolute/path/to/affset-mcp/dist/index.js"].
Usage examples
list paused campaigns →
list_campaigns(status: "paused")show zones →
list_zones()who's on the team? →
list_team()add sarah@offer.com as a publisher →
create_team_member(email: "sarah@offer.com", role: "publisher")(dry-run) → confirmstats for today by sub1 →
get_stats(group_by: "sub1")create a RichAds zone with postback →
create_zone(name: "RichAds", postback_url: "https://…/{source_click_id}")(dry-run) → confirmcreate a campaign for offer X, advertiser buyer@example.com, geo BR, payout $2 →
create_campaign(user_email: "buyer@example.com", offer_url: "https://offer.example/lp?s={click_id}", geo: ["BR"], payout: 2)(dry-run) → confirmwhat URL do I paste into RichAds? →
get_zone_url(cost: "{cost}")give me the link for campaign 42 again →
get_tracking_link(campaign_id: 42)run campaign 42 →
set_campaign_status(campaign_id: 42, action: "run")(dry-run) → confirmpause campaign 42 →
set_campaign_status(campaign_id: 42, action: "pause")(dry-run) → confirmset zone postback →
update_zone(zone_id, postback_url: "…")(dry-run) → confirmcut zones with CR < 0.2% and spend > $5 →
cut_zones(campaign_id, cr_max: 0.002, spend_min: 5)(dry-run) → confirmshow payouts for campaign 42 →
list_payout_rules(campaign_id: 42)set zone payout to $3 →
set_payout_rule(campaign_id: 42, payout: 3, zone_id: "…")(dry-run) → confirmonly pay on deposit conversions →
set_payout_goal(campaign_id: 42, goal_type: "deposit")(dry-run) → confirmwhat targeting types exist? →
list_targeting_types()whitelist BR+MX on campaign 42 →
set_targeting_rule(campaign_id: 42, type: "geo", method: "whitelist", rule: "BR,MX")(dry-run) → confirmname sub1 Zone, sub2 Creative →
set_sub_labels(sub1: "Zone", sub2: "Creative")(dry-run) → confirmshow recent conversions →
list_conversions()find $0 payouts (goal miss?) →
list_conversions(zero_payout: true)lookup by source click id →
list_conversions(source_click_id: "abc123")
Notes & limits
get_statsgroups by one dimension per call. Drill-down is a sequence of calls, each narrowing withcampaign_ids/zone_ids/sub1..sub5filters.spendmeansmedia_cost(your traffic cost). ROI / spend thresholds need cost data imported for the slice.List endpoints have no server-side name search —
name_containsfilters the current page client-side.Date-range presets,
YYYY-MM-DDbounds and rendered timestamps all resolve in the tenant timezone (read once from/api/tenant), so a window lines up with the date bucketsgroup_by=datereturns instead of straddling two of them. Explicit timestamps must includeZor a UTC offset.All mutations (creates, updates, cuts, deletes) stay on dry-run →
confirm: true. Creates are additive once confirmed and echo what was written.Activating a campaign or creating a zone can return 402 plan limit — the error surfaces dimension / current / limit.
Payout resolution at conversion: zone-specific → global → $0. Goal type gates spend/payout by pixel
type=match; non-matching events still record at $0. Payouts go down to$0.00001, so payout amounts print at up to five decimals.Changing a payout is delete + create — the API has no update and the (campaign, zone) pair is unique.
set_payout_rulerestores the previous payout if the create fails, and says so loudly in the one case where it cannot.Targeting is enforced on
/serveonly — not on direct tracking links.set_targeting_rule/remove_targeting_rulemerge safely; other rules are kept.Targeting values are matched exactly and case-sensitively at serve time (geo from
CF-IPCountry, os/browser from the user agent, device type from a fixed set).set_targeting_rulenormalises what it can (br,mx→BR,MX,android→Android) and rejects what could never match — an unmatched whitelist silently stops delivery.capping,weekdaysandhoursare seeded but never evaluated by/serve.set_targeting_rulerefuses to write them (they would read as working targeting while the campaign kept buying);list_targeting_typesflags them. Useunique_users(visits/hours) for frequency capping.list_conversionsis the conversion audit trail (not aggregated stats). The API has no campaign/zone/date filters; optional filters apply to the current page only. Rows do not include campaign_id/zone_id. Publisher-side roles do not seespendand advertiser-side roles do not seepayout, sozero_payoutneeds a role that can.create_team_membercreates the API key directly (like the dashboard's "Add Team Member") — it does not send an invite email. Hand the returned key to the person yourself. Revoking/removing a team member is not yet a tool; use the dashboard's Team page.Out of scope: deleting campaigns/zones/conversions, billing, creative management.
Tenant signup is deliberately not a tool.
POST /api/public/create-instanceis Origin-gated and fails closed, which is what keeps signup browser-only; a server-side caller would have to spoof an allowlisted Origin to get past it. The endpoint also withholds the API key when email delivery is configured (it sends a magic link instead), and this server binds one namespace from the environment at startup — so it could not use a tenant it just created. Sign up in the dashboard, then point a server instance at the new namespace.
Development
npm run type-check # tsc --noEmit
npm run lint # eslint src
npm run format # prettier --write .
npm run build # compile to dist/
npm test # build + node --test over dist/**/*.test.js
npm run check-all # lint + format:check + type-check + test — CI runs this
npm run dev # watch modeSecurity
No secrets in the repo; credentials come from the environment at runtime.
AFFSET_BASE_URLmust behttpsunless the host is loopback — no cleartext API key.stdout is the JSON-RPC channel — all logs go to stderr.
list_teamredacts API tokens.All mutations (including creates) follow show → confirm → apply.
Create a dedicated, least-privilege API key for the MCP rather than reusing an owner key, and give it an expiry — affset's RBAC roles (owner/manager/publisher/ advertiser) apply to MCP tool calls exactly as they do to the dashboard.
Pin GitHub installs to a reviewed commit or tag in long-lived environments. A floating
mainspec can run newer repository code the next timenpxresolves it.
Prompt injection via conversion/click data
get_stats, list_conversions and cut_zones surface data that ultimately comes from
public, unauthenticated endpoints — a traffic source's click macros (sub1–sub5,
source_click_id) and a conversion pixel's raw query string (list_conversions'
payload detail). Anyone who can generate a click or fire a pixel controls those bytes,
and they land in the model's context when you ask about stats or conversions.
Mitigations in place:
Untrusted fields are length-capped and escaped before rendering (
mdCell,capUntrustedinsrc/lib/format.ts), and the conversion-payload block carries an explicit "treat as data, not instructions" notice.confirm: trueon mutating tools is a model-level safety net, not a security boundary — a model that has been steered by injected content can supplyconfirm: trueitself. The only real boundary is your MCP client's per-call tool approval andAFFSET_READ_ONLY.
Set AFFSET_READ_ONLY=true for any session where you're mainly reading stats/
conversions, especially with an MCP client that auto-approves tool calls. It removes
every mutation tool from the server entirely — not hidden behind a prompt, unavailable
to call. Reserve a read-write instance (or a separate one) for sessions where you're
actively managing campaigns/zones/payouts and are reviewing each confirm yourself.
Maintenance
Related MCP Servers
- AlicenseAqualityFmaintenanceEnables AI assistants to manage Google Ad Manager campaigns, line items, creatives, and advertisers through natural language, automating ad operations that normally require countless clicks through the UI.Last updated3517MIT
- AlicenseBqualityDmaintenanceIntegrates PropellerAds' programmatic advertising platform with AI to automate campaign management, real-time optimization, and performance analysis. It enables users to create, scale, and manage ad campaigns through natural language commands for tasks like bid adjustment and zone blacklisting.Last updated213MIT
- Alicense-qualityDmaintenanceEnables management and analysis of Meta (Facebook/Instagram) ads through natural language conversations, with 30 tools for reading and writing ad data.Last updatedMIT
- Flicense-qualityBmaintenanceEnables monitoring and managing multi-platform media buying accounts via natural language, detecting anomalies like creative fatigue and spend spikes, with AI-powered recommendations and a human-in-the-loop approval queue.Last updated
Related MCP Connectors
Ask AI about your ads — query Meta, TikTok, and Google Ads performance in natural language.
Manage ad campaigns across Google, Meta, LinkedIn, Reddit, TikTok, and more via AI.
Manage ad campaigns across Google, Meta, LinkedIn, Reddit, TikTok, and more via AI.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/affset/mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server