05 · Technical Integrators
Affise MCP for Integrators: One Connector, Every Affise API
Structured calls, natural-language queries, and compacted tabular output — all through a single Model Context Protocol endpoint.
What this means for an integrator
If you have ever written glue code to hit the Affise REST API from a script, a BI tool, or an AI assistant, you know the overhead: auth headers, pagination, date-range handling, status-code mapping, and a different request shape for every endpoint family. The Affise MCP server exposes the same data through a single MCP endpoint that both AI clients (Claude Desktop, Cursor, Continue.dev) and scripts can drive without per-endpoint plumbing.
You pick the right tool for the query — natural-language for exploration, structured params for precision — and the server translates it to the correct Affise API call. It maps status names to numeric codes, compacts tabular responses, and returns meaningful errors when something is misconfigured. This article covers what is available, how to connect, and what to watch for in production.
Pick your connection
Two deployment modes: Hosted (Affise runs the server at https://mcp.affise.com) and Local (you run it on your own machine from the public repo).
Hosted — OAuth 2.0 (recommended for interactive clients)
In Claude Desktop or claude.ai: Settings → Connectors → Add custom connector, then enter https://mcp.affise.com/mcp. A browser login prompt handles the OAuth flow; tokens refresh automatically.
Config-file form (e.g. claude_desktop_config.json):
{
"mcpServers": {
"affise": {
"url": "https://mcp.affise.com/mcp",
"transport": { "type": "sse" }
}
}
}
Hosted — Static Bearer token (for scripts and clients without OAuth)
Use the mcp-remote bridge. The same block works in Claude Desktop, Cursor (~/.cursor/mcp.json), and any MCP-capable client:
{
"mcpServers": {
"affise": {
"command": "npx",
"args": [
"-y", "mcp-remote",
"https://mcp.affise.com/mcp",
"--header", "Authorization: Bearer YOUR_TOKEN"
]
}
}
}
Continue.dev has a ready-made config.
Local (stdio) — Node, .dxt extension, or Docker
Clone the public repo, install, and build once:
git clone <repo-url>
npm install
npm run build
Node directly (stdio waits for JSON-RPC on stdin — that is normal; logs go to stderr):
AFFISE_BASE_URL=https://api-<company>.affise.com \
AFFISE_API_KEY=YOUR_API_KEY \
node build/index.js
Claude Desktop .dxt extension — no JSON editing required:
npm run build-dxt
Drag the generated .dxt into Settings → Extensions, then enter your base URL and API key in the UI.
Docker (-i is required so the container reads stdin):
npm run docker:build
docker run --rm -i \
-e AFFISE_BASE_URL=https://api-<company>.affise.com \
-e AFFISE_API_KEY=YOUR_API_KEY \
affise-mcp
For a full walkthrough including OAuth setup and token issuance, see the Client Quickstart. Ready-made configs for Cursor and Continue.dev are in the examples folder.
The toolset
Tools fall into five groups. Analysis prompts are separate from tools — they are MCP prompts that orchestrate multi-step reasoning.
| Group | Tools |
|---|---|
| Natural-language | affise_stats, affise_search_offers, affise_smart_search |
| Structured data | affise_stats_raw, affise_conversions_raw |
| Lookups / detail | affise_status, affise_get_offer, affise_get_conversion, affise_offer_categories, affise_offer_tracking_link, affise_list_partners, affise_get_partner, affise_list_advertisers, affise_get_advertiser |
| Analytics | affise_trafficback, affise_retention_rate, affise_time_to_action |
| Partner self-service | affise_partner_profile, affise_partner_balance, affise_partner_news, affise_partner_offers, affise_partner_live_offers, affise_partner_find_subs |
Partner self-service tools authenticate with a partner-scoped API key and return only what that affiliate can see. The admin-class tools (affise_list_partners, affise_get_advertiser, etc.) require an admin-level key.
Analysis prompts (not tools — invoked as MCP prompts, not function calls):
| Prompt | Common uses |
|---|---|
analyze_offers | Performance, market positioning, compliance review |
analyze_stats | Geo breakdown, conversion funnel, traffic source quality |
analyze_trafficback | Reason codes, partner quality, technical issues |
analyze_conversions | Attribution, partner quality, payout consistency |
workflow_analysis | End-to-end offer discovery → stats |
auto_analysis | Multi-data-type orchestration |
Each prompt accepts analysis_type | focus_areas | comparison_criteria | format, where format is summary, detailed, or actionable.
Structured control when you need it
Natural-language tools are convenient for exploration, but affise_stats_raw and affise_conversions_raw give you explicit control over what comes back.
affise_stats_raw
Parameters: slice (dimensions), fields (metrics), filter (key-value map), date_from / date_to, order (use "-key" prefix for descending).
{
"slice": ["day", "country"],
"fields": ["clicks", "conversions", "income", "cr"],
"filter": {
"partner": ["193"],
"os": ["Unknown"],
"sub5": ["abc"]
},
"date_from": "2026-06-23",
"date_to": "2026-06-29",
"order": ["-clicks", "country"]
}
Sub-ID rules:
sliceacceptssub1throughsub30.filteraccepts onlysub1throughsub8— this is a Filter.php constraint, not an MCP limit.- Date range is capped at 6 months. Requests that exceed it are rejected before hitting the API.
affise_conversions_raw
Returns one row per conversion event. Status names map automatically: confirmed→1, pending→2, declined→3, not_found→4, hold→5.
{
"date_from": "2026-06-29",
"date_to": "2026-06-29",
"status": ["confirmed"],
"partner": 193,
"country": ["US"]
}
For bulk export that bypasses the response mapper, add "raw_export": 1. This returns unprocessed conversion rows but caps the date range at 63 days (vs 365 for the normal path). Use it when you need the raw payload for downstream processing.
Natural-language equivalents for reference:
›"Top 10 by sub5 last week." "Breakdown by sub1 and sub5 last 30 days."
The NL tools (affise_stats) parse these queries and delegate to the same underlying stats endpoint — useful for ad-hoc questions, less predictable for automation pipelines.
Built for machines too
Tabular responses — stats, conversions, partner lists, advertiser lists, trafficback — are returned in a compacted column/rows shape instead of an array of objects:
{
"columns": ["day", "country", "clicks", "conversions", "income", "cr"],
"rows": [
["2026-06-29", "US", 1420, 38, 760.00, 2.68],
["2026-06-29", "DE", 310, 9, 180.00, 2.90]
],
"dropped_columns": ["goals", "epc_partner", "roi"],
"total": 2,
"page": 1,
"per_page": 100
}
Columns that are empty across the entire result set are dropped and listed under dropped_columns. On a 100-conversion pull this typically cuts payload size roughly in half. This matters in two ways. It keeps responses within LLM context budgets when you feed data into a model, and it reduces the bytes a script has to parse. If you need a dropped column, add it explicitly to your fields array.
Interactive UI: MCP-UI app widgets
Beyond text and structured JSON, the server ships MCP-UI app widgets — self-contained HTML components the host renders in a sandboxed iframe. A tool advertises one via _meta.ui.resourceUri; the host fetches the ui:// resource and renders it. The full result still travels in structuredContent, so nothing is lost to the model context. Four widgets ship today:
ui://widgets/stats-grid.html— sortable, client-paginated stats table (backsaffise_stats,affise_stats_raw,affise_conversions_raw,affise_trafficback, and the partner/advertiser list tools)ui://widgets/offers-list.html— offer results as scannable cardsui://widgets/tracking-link.html— copy-ready tracking linkui://widgets/auth-form.html— inline connect / credentials form
The Apps SDK runtime is inlined into each widget (the iframe sandbox blocks CDN imports, so a top-level import would 404 silently). In clients without UI support, every tool degrades gracefully to text plus structured output — the widgets are additive, never required.
| affiliate.id | affiliate.login | traffic.raw | total.earning | total.count | confirmed.count | confirmed.earning | declined.count | cr |
|---|---|---|---|---|---|---|---|---|
| 2195 | NorthPeak Media | 824544 | 913.508 | 38136 | 31240 | 812.44 | 1210 | 4.62 |
| 2575 | BlueOrbit Ads | 47686 | 402.325 | 10239 | 8110 | 380.10 | 640 | 2.15 |
| 160 | Halcyon Media | 98430 | 328.00 | 2110 | 1800 | 300.25 | 140 | 2.14 |
| 3897 | Skylark Digital | 189576 | 305.55 | 1625 | 1402 | 291.00 | 88 | 0.86 |
| 3992 | Vertex Reach | 4665 | 220.7125 | 1128 | 990 | 210.50 | 44 | 24.18 |
| 2951 | Amberline Ads | 640200 | 154.60 | 1980 | 1600 | 148.00 | 190 | 0.31 |
| 2842 | PixelForge | 343 | 98.912 | 71 | 60 | 92.00 | 5 | 20.70 |
| 3311 | Solace Media | 12050 | 88.20 | 940 | 800 | 82.00 | 60 | 7.80 |
| 2210 | Onyx Partners | 35620 | 72.90 | 1980 | 1600 | 68.00 | 190 | 5.55 |
| 3148 | Quanta Media | 35136100 | 61.10 | 2820 | 2400 | 58.00 | 210 | 0.008 |
| 2134 | Driftwood Media | 0 | 48.167 | 365 | 300 | 45.00 | 30 | 0.00 |
| 4012 | Lumen Partners | 96 | 44.375 | 37 | 30 | 41.00 | 3 | 38.54 |
| 2668 | Kestrel Ads | 5120 | 33.10 | 210 | 175 | 30.00 | 18 | 4.10 |
| 3402 | Terra Ads | 1540 | 27.30 | 130 | 110 | 25.00 | 12 | 8.44 |
| 4005 | Cobalt Digital | 1391360 | 22.00 | 249 | 200 | 20.50 | 22 | 0.017 |
| 2876 | Meridian Ads | 169 | 18.6395 | 12 | 10 | 17.00 | 1 | 7.10 |
| 3970 | Nimbus Performance | 243 | 14.44 | 80 | 66 | 13.10 | 8 | 32.92 |
| 4120 | Zephyr Reach | 760 | 12.75 | 44 | 38 | 11.50 | 3 | 5.79 |
| 3855 | Cedar Performance | 220 | 9.15 | 28 | 24 | 8.40 | 2 | 12.72 |
| 2779 | Ironwood Digital | 34932 | 5.04 | 723 | 610 | 4.80 | 55 | 2.07 |
stats-grid widget in a UI-capable client — sortable columns, live row filter, and empty columns auto-dropped. Affiliate names are anonymized for this demo.Gotchas
sub9..sub30are not valid filter keys. They work as slice dimensions and order keys but the Affise filter layer caps atsub8. Passingsub9as a filter key returns an error, not an empty result.- Conversions form uses root-level params. The
/stats/conversionsendpoint does not nest params underfilter[...]the way/stats/customdoes. The MCP tool serializes accordingly — do not pass a nestedfilterobject toaffise_conversions_raw. - 403 on sub-slicing. If slicing by a sub-ID dimension returns a 403, the API key role does not have sub-ID grouping access. This typically requires an admin-class key, not a standard one.
CONFIG_MISSINGerror. MeansAFFISE_BASE_URLand/orAFFISE_API_KEYare not set (local mode), or the OAuth flow has not been completed (hosted mode). Verify the connection withaffise_statusafter setup.- Docker:
-iis required. Without it the container exits immediately because there is nothing on stdin. Logs go to stderr; stdout carries only JSON-RPC frames. - Advertiser IDs are 24-char MongoId hex strings.
affise_list_advertisersreturns them; pass that exact string toaffise_get_advertiser.
Where to go next
- Full setup walkthrough: Client Quickstart — OAuth flow, Bearer token generation, local stdio setup, first queries.
- Ready-made client configs and sample queries: examples folder —
sample-queries.md,cursor.json,continue-dev.yaml.