Skip to main content
Glama
mittwald

etracker MCP Server

by mittwald
README.md
# etracker MCP Server

Streamable-HTTP [MCP](https://modelcontextprotocol.io) server for the
[etracker Analytics Report API](https://help.etracker.com/en/article/rest-report-api/).
Read-only, 4 tools, runs in a container.

> The REST Report API is available in the etracker analytics **Enterprise
> Edition**. Create an access token with scope *Reporting API* under
> **Settings → Account → Integration**.

## Table of contents

- [Run in Docker](#run-in-docker)
- [Authentication](#authentication)
- [Connect from your client](#connect-from-your-client)
  - [Claude Code](#claude-code)
  - [Claude Desktop](#claude-desktop)
  - [Cursor](#cursor)
  - [n8n](#n8n)
  - [VS Code (Copilot Chat)](#vs-code-copilot-chat)
  - [MCP Inspector (debug)](#mcp-inspector-debug)
- [Tools](#tools)
- [Example prompts](#example-prompts)
- [Configuration](#configuration)
- [Operations](#operations)
- [Development](#development)

## Run in Docker

Published image: **`ghcr.io/mittwald/etracker-mcp:latest`** (tags: `latest`, `0.1.0`).

```bash
docker run -d --name etracker-mcp -p 3334:3334 \
  --restart unless-stopped \
  ghcr.io/mittwald/etracker-mcp:latest
```

The server is credential-free at startup — every client provides its own
etracker access token via a header (see [Authentication](#authentication)).

Endpoint: `http://127.0.0.1:3334/mcp` · Healthcheck: `GET /health`.

> Build locally instead: `docker build -t etracker-mcp .` and use
> `etracker-mcp` as the image name.

### docker compose

```yaml
services:
  etracker-mcp:
    image: ghcr.io/mittwald/etracker-mcp:latest
    ports: ["3334:3334"]
    restart: unless-stopped
```

## Authentication

Each client provides its own etracker access token via a request header:

- `X-ET-Token` — access token with scope *Reporting API*.

The server keeps no global credentials, holds nothing in env, and stores
nothing on disk. The token lives only in the per-session `EtrackerClient`
inside the running process. One server can serve many etracker accounts.
Requests without the header are rejected with `401`.

The upstream API base URL defaults to the public endpoint
`https://ws.etracker.com/api/v7` and can be overridden server-side with
`ETRACKER_API_URL`.

## Connect from your client

All client examples below include the required `X-ET-Token` header. Replace
the token with your own.

### Claude Code

```bash
claude mcp add etracker http://127.0.0.1:3334/mcp --transport http \
  --header "X-ET-Token: your-access-token"
```

### Claude Desktop

**Recommended: one-click install via the `.mcpb` extension.** No config
files, no Docker. Download the latest `etracker-mcp-<version>.mcpb` from the
[Releases](https://github.com/mittwald/etracker-mcp/releases) page and
double-click it (or in Claude Desktop: **Settings → Extensions → Advanced
settings → Install Extension…**). You'll be prompted for two values in a form:

- **Server URL** — pre-filled with the hosted instance; leave as is unless you
  run your own.
- **etracker Access Token** — your token with scope *Reporting API* (stored in
  the OS keychain, marked sensitive).

Works on macOS, Windows and Linux with no extra setup — the extension bundles
a small stdio↔HTTP bridge that runs in-process on Claude Desktop's own Node
runtime (no system Node, no `npx`, no local listening socket; the only network
I/O is the outbound HTTPS call to the server). To build the bundle yourself:
`pnpm pack:mcpb` → `dist-mcpb/etracker-mcp-<version>.mcpb`.

<details>
<summary><b>Manual alternative: edit the config with <code>mcp-remote</code></b></summary>

> Claude Desktop's stable config only accepts **stdio** MCP servers. To
> use this HTTP-based server, bridge it via the `mcp-remote` shim
> (auto-installed by `npx`). This also lets you pass the required
> `X-ET-Token` header, which the Connectors UI doesn't support.

**Step-by-step:**

1. Quit Claude Desktop (`⌘Q` on macOS · right-click tray → Quit on Win/Linux).
2. Open the config file:

   | OS | Path |
   | --- | --- |
   | macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
   | Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
   | Linux | `~/.config/Claude/claude_desktop_config.json` |

3. Add (or merge into) `mcpServers`:

   ```json
   {
     "mcpServers": {
       "etracker": {
         "command": "npx",
         "args": [
           "-y",
           "mcp-remote",
           "http://127.0.0.1:3334/mcp",
           "--header",
           "X-ET-Token: your-access-token"
         ]
       }
     }
   }
   ```

4. Save the file and reopen Claude Desktop. The first launch downloads
   `mcp-remote` (one-time, ~5 s).
5. Open a new chat → type `/mcp` and press Enter. `etracker` should appear with
   status **connected** and 4 tools listed.

If it shows **failed**: check that the MCP server is running
(`curl http://127.0.0.1:3334/health` → `{"ok": true}`) and that Node.js is
installed system-wide (`npx` must be on your PATH).

</details>

### Cursor

`~/.cursor/mcp.json`:

```json
{
  "mcpServers": {
    "etracker": {
      "url": "http://127.0.0.1:3334/mcp",
      "headers": {
        "X-ET-Token": "your-access-token"
      }
    }
  }
}
```

### n8n

In an **AI Agent** workflow add the **MCP Client Tool** node:

- **Endpoint**: `http://127.0.0.1:3334/mcp` (or the container hostname if
  n8n runs in Docker, e.g. `http://etracker-mcp:3334/mcp` on the same network).
- **Server Transport**: `HTTP Streamable`.
- **Headers**: add `X-ET-Token`.

Connect the node to the `tools` input of the AI Agent. n8n introspects
`tools/list` automatically, so the etracker tools become available to the
agent without further config.

### VS Code (Copilot Chat)

`.vscode/mcp.json`:

```json
{
  "servers": {
    "etracker": {
      "type": "http",
      "url": "http://127.0.0.1:3334/mcp",
      "headers": {
        "X-ET-Token": "your-access-token"
      }
    }
  }
}
```

### MCP Inspector (debug)

```bash
npx @modelcontextprotocol/inspector
# Streamable HTTP → http://127.0.0.1:3334/mcp
# Add the X-ET-Token header in the Inspector "Authentication" panel.
```

## Tools

The etracker Report API is report-centric: pick a report (e.g. `EAPage`,
`EAGeo`, `EADeviceType`), then query its rows. Each report exposes
*attributes* (dimensions) and *keyfigures* (metrics).

| Tool | Purpose |
| --- | --- |
| `list_reports` | Map of report ID → display name. Call first. |
| `get_report_info` | Metadata for a report: create date, segments, attributes. |
| `get_report_metadata` | Column metadata: ids, labels, types, sortable/filterable flags. |
| `get_report_data` | Report rows with date range, paging, sorting, column selection and filters. |
| `compare_report_data` | Two periods compared per row: current/previous/delta/pctChange per figure, sorted by largest change. |

A typical flow: `list_reports` → `get_report_metadata` (to learn valid
attribute/figure IDs) → `get_report_data` (or `compare_report_data` for
period-over-period analysis). `compare_report_data` defaults the comparison
period to the equally long span immediately before the current range.

## Example prompts

Marketing-oriented questions for the connected assistant:

- "Which reports are available in my etracker account?"
- "Show me the top 10 pages by unique visits in **May 2024** (`EAPage`)."
- "Break down visits by device type for the last 7 days."
- "Which countries drove the most traffic last month? Sort descending."
- "List pages whose name matches \"Checkout\" with more than 100 unique visits."
- "Compare page impressions for `/en/` vs `/de/` URLs over the last 30 days."

## Data notes

**Figures are returned verbatim.** The server never sums, merges or invents
values — keyfigures always match what etracker (and its web UI) reports. The
only transformation is cosmetic: invisible/zero-width characters are stripped
from attribute **labels** so duplicates are recognizable.

- **Composite row IDs**: `get_report_data` rows carry an `id` that can be a
  comma-joined composite of the report's dimensions (e.g. `pageNameId,urlId`
  for `EAPage`). `compare_report_data` joins rows by this `id` (falling back to
  the requested attribute values when no `id` is present).
- **Invisible characters → phantom duplicate rows**: etracker may list the same
  entity twice under attribute values that differ only by an invisible
  character — e.g. a `page_name` and a second copy prefixed with an invisible
  separator (`U+2063`), because the tracked page's JavaScript prepends one to
  `document.title`, from which etracker auto-derives the page name. You then see
  two rows for one page (e.g. 627 + 7 page impressions), exactly as in
  etracker's own report. The server strips the invisible characters
  (`U+200B–200F`, `U+2060–2063`, `U+FEFF`, soft hyphen, bidi marks) from the
  **labels** only — it does **not** merge the rows, because the right total
  cannot be reconstructed from them: page impressions are additive, but
  **visits and unique visitors are not** (the same session/visitor is
  re-attributed to both rows, so summing double-counts). For a correct,
  de-duplicated per-page total, **query by `url`** (`attributes=url`, drop
  `page_name`) so etracker aggregates and de-duplicates server-side. To fix it
  at the source, set a clean `et_pagename` on the tracked page so etracker stops
  deriving the name from the mutated title.

## Configuration

| Env | Required | Default | |
| --- | --- | --- | --- |
| `MCP_PORT` | no | `3334` | TCP port. |
| `LOG_LEVEL` | no | `info` | One of `debug`, `info`, `warn`, `error`. |
| `ETRACKER_API_URL` | no | `https://ws.etracker.com/api/v7` | Base URL of the etracker Report API. |
| `ETRACKER_REQUEST_TIMEOUT_MS` | no | `120000` | Per-request timeout. Raise it if large ranges (e.g. a full year of daily rows) still time out. |

The etracker access token is not configured via env — it is passed by each
client via the `X-ET-Token` request header. See
[Authentication](#authentication).

## Operations

- **Logging**: structured JSON to stdout/stderr (`info`/`debug` → stdout,
  `warn`/`error` → stderr). One line per event, no header values logged.
  Tail with `docker logs -f etracker-mcp`.
- **Healthcheck**: `GET /health` returns `{"ok": true, "version": "..."}`.
  Wired into the Dockerfile `HEALTHCHECK`.
- **Limits**: request bodies > 1 MB → `413`; outbound calls to etracker
  timeout after 30 s. The etracker API itself limits to 50 calls per 5
  minutes, 10 parallel requests, and 100,000 rows per response.
- **Security model**: server holds no credentials. Run multiple replicas
  behind a load balancer if needed — sessions are sticky via the
  `Mcp-Session-Id` header, so terminate sessions on the same backend
  (or accept that a reconnect re-initializes a session).
- **Image**: pin a digest in production
  (`ghcr.io/mittwald/etracker-mcp@sha256:…`) rather than `:latest`.
- **Graceful shutdown**: `SIGTERM`/`SIGINT` close all open MCP sessions and
  drain in-flight requests before exit (10 s hard cap).

## Development

```bash
pnpm install
pnpm dev           # tsx watch
pnpm test          # unit tests
pnpm test:live     # live smoke test (client → API), needs ETRACKER_TOKEN env
pnpm build         # tsc → dist/

# Holistic end-to-end test: drives the running server over the real MCP
# transport against the live API, exercising every tool.
pnpm build && node dist/index.js &      # start the server
ETRACKER_TOKEN=... pnpm test:e2e        # MCP_URL overridable (default :3334)
```

All tools are read-only. Add a tool: extend `src/tools.ts`, add a unit test
in `tests/tools.test.ts`, add a live test in `tests/live/live.test.ts` that
asserts the actual response shape.