Skip to main content
Automation October 12, 2026 · 12 min read

How to Connect Adtraction to Claude Code With Your Own MCP Server

Build a read-only Adtraction MCP server for Claude Code in four steps. Keep the X-Token local, stay inside the 30 requests per minute quota, return aggregates.

Edward Chalupa

Edward Chalupa

Founder, Whtnxt · Dallas, TX

How to Connect Adtraction to Claude Code With Your Own MCP Server

Adtraction ships a reporting API. Claude Code ships an MCP client. Nothing official joins them, so every result for this query sells a connector that holds your API token. Here is the server I built instead.

Four read-only tools, a key that never leaves my machine, and responses shaped to survive Claude Code’s output limit. If the Adtraction API itself is new to you, read how I connect Adtraction to Google Sheets first, because that pull is the data layer this server sits on top of.

What Do You Need Before You Start?

Four requirements, and two of them you already own if you run any of my other Adtraction guides.

RequirementWhere it comes fromTime
Adtraction partner account plus an API tokenAccount > Settings > API tab > Create new API token3 minutes
Python 3.11 or newerThe MCP SDK needs it2 minutes
uv or uvxThe installer I use for every small MCP server1 minute
Claude Code on the same machineAnthropic’s CLI2 minutes

The token is the one to think about before you write any code. Adtraction authenticates with a static key sent in an X-Token header, documented in the Adtraction API getting-started guide alongside its rate limits. The key does not expire and there is no refresh dance. That is exactly why the nightly pipeline in my Adtraction to Looker Studio bridge survives unattended. It is also exactly why handing that key to a hosted connector deserves a deliberate decision rather than a default one.

Diagram: Claude Code calls the local Adtraction MCP server, which calls the Adtraction v3 API, with the X-Token held in a local environment variable

Why Is a Hosted Connector the Wrong First Move?

Search this topic and the top of the page belongs to connector platforms. Each performs the same trick. You authorize their service, they store your Adtraction credential, and their cloud calls the API on your behalf so Claude can answer questions.

I will not route a client’s commission ledger through infrastructure I do not control when the local build takes an afternoon. Here is the position I would defend under pressure: a connector that holds a long-lived static key against a payment ledger is a worse security posture than a process on your own laptop, even when the vendor is competent. The credential those services hold is a long-lived static key with full reporting read access, not a short-lived token that expires overnight. A connector breach is not a bad day. It is a key you must hunt down and rotate at every vendor that ever received a copy.

There is a second reason, and it only surfaces once you are doing real work. A hosted connector answers from a snapshot it already pulled on its own schedule. A server you wrote answers in the shape you asked for, because you decide what each tool returns. That difference is the entire build below.

If you would genuinely rather not run a server, the honest alternative is the reporting chain I already ship: Adtraction into Looker Studio for Google-first clients, or Adtraction into Power BI when the client lives in Microsoft. Both keep the token on your own box.

Step 1: How Do You Expose Four Read-Only Tools?

The server is a small Python process that calls the Adtraction API and publishes the results as MCP tools. I use FastMCP because it turns a decorated function into a tool and reads its schema from the type hints.

# adtraction_mcp.py
import os
import httpx
from fastmcp import FastMCP

API = "https://api.adtraction.net/v3"
TOKEN = os.environ["ADTRACTION_X_TOKEN"]
HEADERS = {"X-Token": TOKEN, "Content-Type": "application/json"}

mcp = FastMCP("adtraction")

@mcp.tool()
def statistics(from_date: str, to_date: str, market: str = "SE") -> dict:
    """Daily Adtraction totals between two ISO dates."""
    body = {
        "fromDate": from_date,
        "toDate": to_date,
        "market": market,
        "transactionStatus": 3,
    }
    r = httpx.post(f"{API}/partner/statistics/days/", json=body, headers=HEADERS)
    r.raise_for_status()
    return {"days": project(r.json()), "count": len(r.json())}

transactionStatus: 3 is the value to start with. It means approved plus pending, which matches what the Adtraction dashboard shows by default. I found that out by sending the word "approved" on my first pull and eating a validation error, the same trap I wrote up in the Sheets connector guide along with the full field map.

Three more tools finish the surface, and the discipline is identical each time:

  • program_summary(market) calls the aggregate statistics endpoint and returns commission, unique clicks, sales and EPC per program.
  • top_programs(from_date, to_date, n) calls the same endpoint, sorts by commission in Python, and returns the first n.
  • balance() reads the account balance so Claude can answer what is unclaimed without a dashboard login.

Every tool is a read. I did not build a single write tool, and that choice is deliberate rather than lazy. Claude Code can be wrong with confidence. A wrong answer about last week’s commission costs you a correction. A wrong call that creates tracking links or edits a campaign costs a client relationship. If you need writes, put them behind a confirmation step and ship them separately, which is the same reasoning behind the approval gate in my multi-agent n8n build.

Step 2: Where Does the API Token Actually Go?

This is the step that separates a server you can share from a server you leak.

Claude Code stores project-scoped servers in a .mcp.json file at the project root, and Anthropic’s MCP documentation tells you to check that file into version control so the team gets the same tools. A token pasted into a file the official docs tell you to commit will be in your git history by Friday.

The fix is environment variable expansion, which the same documentation supports inside the server config:

{
  "mcpServers": {
    "adtraction": {
      "command": "uvx",
      "args": ["adtraction-mcp"],
      "env": {
        "ADTRACTION_X_TOKEN": "${ADTRACTION_X_TOKEN}"
      }
    }
  }
}

That reference resolves from your shell when the server launches. The committed file contains a variable name, not a secret. Set it in your shell profile or in an ignored .env, and paste the value nowhere.

You can also register it from the terminal without touching JSON:

claude mcp add-json adtraction '{"type":"stdio","command":"uvx","args":["adtraction-mcp"],"env":{"ADTRACTION_X_TOKEN":"${ADTRACTION_X_TOKEN}"}}'

That string carries the variable name, not the key, so the entry is safe to share with a teammate.

claude mcp add-json writes to your user config rather than the project file. That is the scope I want for a server holding a payment-adjacent key. Teammates who need it add the same line themselves. The Postiz MCP setup for Claude Desktop covers the desktop equivalent, and the pattern is identical: one process, several clients, credentials in the environment.

Step 3: Why Does Your Answer Vanish on Big Date Ranges?

Here is the failure that will cost you an hour if nobody warns you. Claude Code caps what a single tool result may carry into the conversation. Anthropic’s MCP reference states that a text result longer than 50,000 characters is written to a file instead of reaching the model, and raising MAX_MCP_OUTPUT_TOKENS does not move that threshold.

A naive statistics tool hits the ceiling faster than you would guess. Ask for a quarter across every program and market and you get one row per day per program. Claude Code quietly files that JSON, then answers your question about November with a shrug about missing data. The tool did not fail. The shape did.

Three rules keep every response inside the limit:

  1. Aggregate in Python, not in the prompt. Sort and slice before returning. top_programs(..., n=10) belongs inside the tool, not inside your question.
  2. Project fields. Return the handful of fields a reporting question actually uses and drop the rest.
  3. Bound the window. A range wider than one quarter should raise rather than try. The error tells Claude to narrow it, which beats a truncated blob every time.
FIELDS = ("date", "programName", "commission", "clicksUnique", "sales", "epc")

def project(rows):
    return [{k: row.get(k) for k in FIELDS} for row in rows]

The same instinct governs my scheduled pipelines. The automated marketing reporting dashboard writes a summary tab rather than shipping raw transactions into the reporting layer, because the consumer’s limits are part of the design.

Step 4: How Many Requests Does an Agent Really Make?

Adtraction documents its limits plainly. The getting-started guide sets most endpoints at 30 requests per minute and a smaller group at 10, and responses carry an X-RateLimit-Limit header you can read in the API reference.

A scheduled pull never gets near that. It spends one or two requests a day, which is why my nightly job has never tripped a limit. An interactive agent changes the arithmetic completely, because a conversation is not one request. Claude refines, retries, and drills into a program you never named, and each refinement is another call.

Two guards are worth building before you use the server for real work:

  • Read the rate-limit headers on every response. When the remaining allowance hits zero, return a tool error saying so instead of firing the next call into a rejection.
  • Cache by date range inside the process. Claude asking about the same month three times in one session should cost one API call, not three.
cache = {}

def cached_stats(key, fetch):
    if key not in cache:
        cache[key] = fetch()
    return cache[key]

That cache lives and dies with the session, which is the right lifetime for reporting data. Affiliate commissions settle overnight in Stockholm time, so a longer cache serves you stale numbers and buys nothing.

How Do You Verify the Server Before Trusting a Number?

Run four checks in this order. Each fails for a different reason, and the order tells you which layer broke.

  1. claude mcp list should print adtraction as connected. If the server is absent, the config you edited is not the scope you launched from.
  2. Inside a session, run /mcp. All four tools should be listed. Fewer tools than you wrote almost always means an import error at startup, and the traceback is in the server’s stderr.
  3. Ask for yesterday’s totals for one market, then compare against the Adtraction dashboard for the same date. Numbers must match exactly, because a gap here means a transactionStatus or currency mismatch rather than a bad question.
  4. Ask for a range a year wide. It should refuse with your date-range error, not stall and not return a partial year.

Only then put it in front of a client. A number nobody validated against the source becomes a rumor with charts, and an assistant that quotes it confidently makes the rumor faster.

Common Mistakes I Made First

SymptomCauseFix
Auth rejected on every callToken pasted with a trailing space, or lacking partner scopeRecreate the token; send it as X-Token, never as a bearer value
Method rejected on a stats endpointGET used against an endpoint the API restricts to POSTEach Adtraction endpoint allows one method, and the API guide prints it in the endpoint URL
Claude says it has no Adtraction toolsServer crashed on import before registering toolsRun the server command directly and read the traceback
Answers cite missing datesResponse crossed the character ceiling and was filed awayNarrow the range or project fields inside the tool
Works in my shell, fails for a teammateToken resolved from a different environmentKeep variable expansion in the config; never inline the value

Frequently Asked Questions

Can Claude Code write to Adtraction through this server? Only if you build write tools. Mine are read-only on purpose, which is the cheapest safety decision in the whole build.

Does this work with Claude Desktop as well as Claude Code? Yes. The server speaks MCP, so any MCP-capable client can use it. Claude Desktop needs the same JSON block in its own config file, and Claude Code additionally accepts claude mcp add-json from the terminal.

Should I use a remote HTTP transport instead of a local process? Not for one partner account on one machine. The MCP transport specification reserves streamable HTTP for servers many clients reach at once. Remote means exposing the token to a network you administer less carefully than your laptop. Remote earns its place for a team, and then it needs authentication of its own.

Will the model get the API parameters wrong? It will try. That is why each tool takes two or three narrow arguments and fills sensible defaults itself. A tool that accepts a free-form request body invites invented fields.

What does this cost to run? Python, uvx, and API calls you already have quota for. There is no subscription, which is the entire economic argument against the connector platforms sitting at the top of the search results.

Where This Fits in the Stack

An MCP server is the shortest path from an assistant to a real revenue number. The pattern repeats across everything I build: expose a narrow read surface, keep the credential local, and return the shape the consumer can actually hold. I did the same thing for email in the Listmonk MCP guide, and the concept behind all of it is in MCP servers for marketers.

If you want the assistant to act on those numbers rather than only read them, that is an AI agent development question with different guardrails. For reporting alone, the build above is an afternoon and every piece of it runs on hardware you already own. If this is one module of a bigger stack you are consolidating, the same read-only discipline applies across the automation services I build for clients, and the pricing page lays out what a done-for-you engagement costs against the connector subscriptions you would otherwise stack.

adtractionclaude codemcp serveraffiliate reportingfastmcpapi integrationai agents
Share: