Skip to main content

Build a Klar Attribution MCP Server with Claude

This guide shows you how to connect the Klar Attribution API to an AI assistant like Claude using the Model Context Protocol (MCP).

Written by Frank Birzle

Once it's set up, you can ask questions like "Which channels had the best ROAS last month under data-driven attribution?" and Claude will query your Klar data and answer directly — no dashboards, exports, or copy-paste.

You don't have to write the server by hand. We provide a ready-made CLAUDE.md specification below: drop it into an empty project folder, point Claude Code (or any coding agent) at it, and ask it to build the server. The file contains everything the agent needs — the API reference, the constraints, and the tools to expose.

What is MCP?

MCP is an open standard that lets AI assistants call external tools and data sources through a single, consistent interface. An MCP server is a small program that exposes "tools" the assistant can use. Build one that wraps the Klar Attribution API, and any MCP-capable client — Claude Desktop, Claude Code, and others — can pull your attribution data on demand.

Before you start

You'll need:

  • Your Klar refresh token. In Klar, go to Store Configurator → Store Settings and copy the Refresh Token. One refresh token is tied to one store and one user, so treat it like a password.

  • Node.js 20 or newer installed.

  • Claude Code (npm install -g @anthropic-ai/claude-code) or another coding agent to build the server, plus Claude Desktop if you want to use the finished server there.

Step 1 — Create a project folder and add CLAUDE.md

Create an empty folder for the project, then save the file below inside it as CLAUDE.md. This is the build specification — it tells the coding agent exactly what to build and how the Klar API behaves.

Claude.md:

# CLAUDE.md — Klar Attribution API MCP Server

## Project goal

Build a Model Context Protocol (MCP) server that wraps the Klar Attribution API
so that any MCP-capable client (Claude Desktop, Claude Code, etc.) can query a
Klar store's attribution data in natural language. The server exposes the
attribution report as typed tools, handles authentication, and absorbs Klar's
API constraints (short-lived tokens, the 31-day window limit, and the rate
limit) so the user never has to think about them.

## Tech stack

- TypeScript + Node.js (Node 20+).
- @modelcontextprotocol/sdk (latest) — use McpServer and StdioServerTransport.
- zod for input validation.
- stdio transport (local), so the server plugs straight into Claude Desktop and
Claude Code.
- Native fetch (built into Node 20+) — no extra HTTP client needed.

Python is a fine alternative (official mcp / FastMCP SDK). If you go that route,
keep the same tool surface and the same constraint handling.

## Build steps

1. Scaffold the project: "type": "module" in package.json, a tsconfig.json
targeting NodeNext, output to ./build, and a build script (tsc).
2. Implement a Klar API client module (auth + report) honouring the constraints
in the API reference below.
3. Implement the MCP server with the tools described under "Tool surface".
4. Compile (npm run build) and confirm it builds cleanly.
5. Write a README that includes the exact claude_desktop_config.json entry.

Do not call the live API during the build. The user supplies a real refresh
token at runtime via an environment variable.

## Klar Attribution API reference

Base URL: https://api.getklar.com

### Authentication — exchange the refresh token for an access token

- Endpoint: POST /public/auth/token
- Request header: token: <KLAR_REFRESH_TOKEN> (the store's static refresh token)
- Success: 201 Created
- Response body: { "accessToken": "eyJ...", "expiresIn": 300000 }

The accessToken is a JWT valid for 5 minutes (300,000 ms). Cache it and reuse it
across calls; only request a new one when the current token is within ~30
seconds of expiry.

The refresh token is one per store/user and is a secret. Read it from the
KLAR_REFRESH_TOKEN environment variable. Never hardcode it and never write it to
logs.

### Report — fetch attribution data

- Endpoint: GET /public/attribution
- Header: Authorization: Bearer <accessToken>

Query parameters:

- startDate (string, required): format yyyy-mm-dd.
- endDate (string, required): format yyyy-mm-dd. EXCLUSIVE — results cover
[startDate, endDate). Must be within 31 days of startDate.
- metric (string, optional): one of first_touch, last_touch, data_driven,
linear, any_click, any_click_unique, u_shape, time_decay, marketing_mix.
- window (string, optional): one of unlimited, 1_day, 7_day, 28_day.
- date_breakdown (string, optional): one of order, touch.

A 200 OK returns a JSON array of row objects (schema below).

### Constraints the server MUST enforce

1. Rate limit: 2 requests per 30 seconds. This applies to the API as a whole, so
budget the auth call into it too. Implement a small token-bucket or queue so
chunked or back-to-back calls never exceed the limit.
2. 31-day window maximum. For longer ranges, split into consecutive windows of
<= 31 days (use 30 to leave a safety margin), call each window, and
concatenate the rows. Because endDate is exclusive, chunk as [d0, d0+30),
[d0+30, d0+60), ... with no overlap and no gap. Warn the user in the tool
description that wide ranges take longer because of the rate limit.
3. Token caching as described under Authentication.
4. Logging: on stdio transport, never write to stdout — it corrupts the JSON-RPC
stream. Use console.error (stderr) only.
5. Errors: catch failures and return
{ isError: true, content: [{ type: "text", text: "..." }] } instead of
throwing, so a single bad call doesn't kill the client session.

### Response row schema

Dimensions: channelName, date, campaignName, campaignId, adGroupName,
adGroupId, adName, adId.

Metrics: orders, nc, rc, netRevenue, grossRevenue, cost, clicks, impressions,
cm1, cm2, clv_30, clv_60, clv_90, acm2, ncGrossRevenue, rcGrossRevenue,
ncNetRevenue, rcNetRevenue.

## Metric glossary

These are working definitions for the tool descriptions. For authoritative
formulas, see Klar's margin-metrics documentation in the help center.

- orders — attributed orders. Fractional values are expected: attribution
models distribute partial credit for an order across the touchpoints that led
to it.
- nc / rc — attributed New-Customer and Returning-Customer orders (also
fractional).
- grossRevenue / netRevenue — attributed gross and net revenue.
- cost — ad spend for the row. ROAS is netRevenue / cost.
- clicks / impressions — ad engagement.
- cm1 / cm2 — contribution-margin tiers (CM1 after product/variable costs, CM2
after marketing cost).
- clv_30 / clv_60 / clv_90 — projected customer lifetime value at 30 / 60 / 90
days.
- acm2 — adjusted CM2.
- ncGrossRevenue / rcGrossRevenue / ncNetRevenue / rcNetRevenue — gross and net
revenue split by new vs. returning customers.

## Tool surface

Core — build this first:

- get_attribution_report
- Params: startDate (required, yyyy-mm-dd), endDate (required, yyyy-mm-dd),
metric (optional enum), window (optional enum), date_breakdown (optional
enum). Validate with zod and surface the exact accepted values in the schema.
- Behaviour: auto-chunks any range longer than 31 days, respects the rate
limit, returns the combined rows.

Optional extensions (add if useful to the user):

- summarize_by_channel — same params; aggregates rows by channelName (sum
orders, cost, netRevenue, grossRevenue; derive ROAS = netRevenue / cost).
- compare_periods — two date ranges; returns per-channel deltas.

Bake the key interpretation rules — fractional orders, exclusive endDate, ROAS =
netRevenue / cost — into the tool descriptions so the model reads the results
correctly.

## Run & test

- npm run build
- npx @modelcontextprotocol/inspector node ./build/index.js

## README must include

A one-line description; the exact claude_desktop_config.json snippet; the tool
list with one-line descriptions; the required env var (KLAR_REFRESH_TOKEN); and
a short troubleshooting section covering token expiry, rate-limit pauses on wide
date ranges, and the no-stdout-logging rule.

Step 2 — Let the agent build it

Open the folder in Claude Code (or your agent of choice) and give it a short prompt:

Read CLAUDE.md and build the MCP server it describes. Use TypeScript with
the official MCP SDK and stdio transport. When you're done, run the build
and show me the claude_desktop_config.json entry I need.

The agent will scaffold the project, implement the Klar API client (with token caching, rate limiting, and date-range chunking), define the tools, and compile the server. Review the generated code, then build it.

Step 3 — Connect it to Claude Desktop

Open your Claude Desktop config file and add the server under mcpServers. On macOS it's at ~/Library/Application Support/Claude/claude_desktop_config.json; on Windows it's at %APPDATA%\Claude\claude_desktop_config.json.

{
"mcpServers": {
"klar-attribution": {
"command": "node",
"args": ["/ABSOLUTE/PATH/TO/build/index.js"],
"env": {
"KLAR_REFRESH_TOKEN": "your-store-refresh-token"
}
}
}
}

Use the absolute path to the compiled build/index.js, paste in your store's refresh token, save the file, and restart Claude Desktop. The Klar tools will appear once the server starts cleanly.

If you're using Claude Code instead, you can register the same server from the command line with claude mcp add.

Step 4 — Ask away

Once connected, try prompts like:

  • "Show me last week's attribution by channel under last-touch."

  • "Compare new-customer net revenue in March vs. February."

  • "Which campaigns had a ROAS above 3 in Q1 with data-driven attribution?"

Claude will call your server, fetch the data from Klar, and answer.

Good to know

  • Access tokens expire after 5 minutes. The server refreshes them automatically — you only ever provide the long-lived refresh token.

  • Wide date ranges take a little longer. Klar limits requests to 2 every 30 seconds and 31 days per request, so a full year is fetched in throttled monthly chunks. This is expected.

  • Attributed orders are often fractional. Attribution models split credit for an order across the touchpoints that led to it, so a channel can show, for example, 2.13 orders.

  • Keep your refresh token private. It's tied to your store; never commit it to a repository or share it.

Did this answer your question?