Take your app to the next level with Affise MMP (Mobile Measurement Partner)
Join Now ⚡

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.

GroupTools
Natural-languageaffise_stats, affise_search_offers, affise_smart_search
Structured dataaffise_stats_raw, affise_conversions_raw
Lookups / detailaffise_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
Analyticsaffise_trafficback, affise_retention_rate, affise_time_to_action
Partner self-serviceaffise_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):

PromptCommon uses
analyze_offersPerformance, market positioning, compliance review
analyze_statsGeo breakdown, conversion funnel, traffic source quality
analyze_trafficbackReason codes, partner quality, technical issues
analyze_conversionsAttribution, partner quality, payout consistency
workflow_analysisEnd-to-end offer discovery → stats
auto_analysisMulti-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:

  • slice accepts sub1 through sub30.
  • filter accepts only sub1 through sub8 — 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 (backs affise_stats, affise_stats_raw, affise_conversions_raw, affise_trafficback, and the partner/advertiser list tools)
  • ui://widgets/offers-list.html — offer results as scannable cards
  • ui://widgets/tracking-link.html — copy-ready tracking link
  • ui://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.

Affiseaffise_stats_raw</>
Affise Stats
page 1 · 20/page
affiliate.idaffiliate.logintraffic.rawtotal.earningtotal.countconfirmed.countconfirmed.earningdeclined.countcr
2195NorthPeak Media824544913.5083813631240812.4412104.62
2575BlueOrbit Ads47686402.325102398110380.106402.15
160Halcyon Media98430328.0021101800300.251402.14
3897Skylark Digital189576305.5516251402291.00880.86
3992Vertex Reach4665220.71251128990210.504424.18
2951Amberline Ads640200154.6019801600148.001900.31
2842PixelForge34398.912716092.00520.70
3311Solace Media1205088.2094080082.00607.80
2210Onyx Partners3562072.901980160068.001905.55
3148Quanta Media3513610061.102820240058.002100.008
2134Driftwood Media048.16736530045.00300.00
4012Lumen Partners9644.375373041.00338.54
2668Kestrel Ads512033.1021017530.00184.10
3402Terra Ads154027.3013011025.00128.44
4005Cobalt Digital139136022.0024920020.50220.017
2876Meridian Ads16918.6395121017.0017.10
3970Nimbus Performance24314.44806613.10832.92
4120Zephyr Reach76012.75443811.5035.79
3855Cedar Performance2209.1528248.40212.72
2779Ironwood Digital349325.047236104.80552.07
20 of 87 rows · 9 columnsEmpty columns dropped: traffic.uniq, traffic.revenue, traffic.charge, traffic.earning, total.charge, total.null, total.revenue, confirmed.charge, confirmed.null, confirmed.revenue, pending.charge, pending.earning, pending.null, pending.revenue, pending.count, declined.charge, declined.null, declined.revenue, hold.charge, hold.earning, hold.null, hold.revenue, hold.count, views, ctr, ecpm, impressions.revenue
How the same answer renders as the 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..sub30 are not valid filter keys. They work as slice dimensions and order keys but the Affise filter layer caps at sub8. Passing sub9 as a filter key returns an error, not an empty result.
  • Conversions form uses root-level params. The /stats/conversions endpoint does not nest params under filter[...] the way /stats/custom does. The MCP tool serializes accordingly — do not pass a nested filter object to affise_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_MISSING error. Means AFFISE_BASE_URL and/or AFFISE_API_KEY are not set (local mode), or the OAuth flow has not been completed (hosted mode). Verify the connection with affise_status after setup.
  • Docker: -i is 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_advertisers returns them; pass that exact string to affise_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 foldersample-queries.md, cursor.json, continue-dev.yaml.