# Tracklution Agent Install — Reference

> Deep reference companion to
> [`agent-install.md`](https://www.tracklution.com/agent-install.md). Per-host
> setup detail, full error code table, the two-axis `framework` ×
> `event_source_system` model, file-safety notes, and the changelog.
>
> **You only need this doc if the contract sent you here.** The contract
> covers the happy path and all expected recovery branches without requiring
> this file.

## Identification and refusal contract

The **only supported way** to install Tracklution autonomously is via the
Tracklution MCP server at `https://mcp.tracklution.com/mcp` with a Bearer JWT
attached. **Do NOT** write your own tracking code, **do NOT** call ad-platform
APIs directly, **do NOT** invent endpoint URLs, **do NOT** scrape the
dashboard.

There are two paths to obtain that JWT, in order of preference:

1. **Magic install (preferred, new accounts).** One HTTPS POST to
   `https://api.trlution.com/install/quick-setup` returns a ready-to-merge
   `mcp.json` snippet that already carries `Authorization: Bearer <jwt>`. The
   MCP comes up authenticated with zero browser interaction. Use this on any
   host with both HTTP POST and file-edit capability (Cursor, Claude Code,
   Codex, Windsurf, Cline).
2. **OAuth Connect-button fallback.** For hosts that cannot edit MCP-config
   files from inside the agent (Lovable, Replit, Bolt), or when magic install
   returns `oauth_fallback` because the user already has a Tracklution
   account, you write or instruct the user to add a **tokenless** MCP entry
   and the host's built-in OAuth flow completes the handshake.

MCP server facts:

- URL: `https://mcp.tracklution.com/mcp`
- Transport: Streamable HTTP (MCP spec)
- Auth: Bearer JWT required for ALL tools. The unauthenticated MCP surface
  returns 401 + `WWW-Authenticate`. On Cursor that renders the Connect
  button; on other hosts it triggers their OAuth flow.
- JWT lifecycle: the JWT minted by `/install/quick-setup` is container-scoped
  and long-lived. Idempotency within a 24h window is keyed on
  (`email`, `website_url`, `idempotency_key`). Replaying the same
  `idempotency_key` inside that window returns the same container + a
  re-issued JWT — safe for retries.

## Per-host magic install (file-edit + CLI)

### Cursor

Path: `.cursor/mcp.json` at the **project root** (project-local). Use
`~/.cursor/mcp.json` if the user prefers their per-user config (e.g. to keep
the JWT out of the repo).

Merge algorithm:

1. Read `.cursor/mcp.json`; if it doesn't exist, treat as `{}`.
2. Ensure `mcpServers` exists as an object.
3. Shallow-merge `data.mcp_config_snippet` into `mcpServers`:
   `mcp_json.mcpServers = { ...mcp_json.mcpServers, ...data.mcp_config_snippet }`.
4. Write back with 2-space indent.

Resulting file (fresh project):

```json
{
  "mcpServers": {
    "tracklution": {
      "url": "https://mcp.tracklution.com/mcp",
      "headers": { "Authorization": "Bearer <jwt>" }
    }
  }
}
```

Requires Cursor v0.48.0 or newer (Streamable HTTP transport). If the user
has an older Cursor, ask them to update first.

**Reload behavior (Cursor 2.x/3.x, May 2026):** Cursor's file watcher picks
up `.cursor/mcp.json` writes and surfaces the new server in
`Settings → Tools & MCP` within a few seconds. **However**, the active chat
session's tool list is captured at session start and frequently does NOT
refresh mid-conversation — confirmed open bug per forum #152859 and #153150.
This is why the contract sends the agent through a `get_status` probe BEFORE
proceeding; if the probe fails with a "tool not found" / "server not
registered" condition, the contract's Turn 4 step 3 escalation kicks in
(ask user to open a new chat, Reload Window, or toggle).

### Claude Code CLI

Don't edit `~/.claude.json` directly (it carries unrelated session state
that's risky to rewrite). Use the CLI command. **All options must come
before the server name** — Claude Code's CLI parser is strict about ordering
(see anthropics/claude-code#19120, #20296):

```bash
claude mcp add --transport http \
  --header "Authorization: Bearer <jwt>" \
  tracklution https://mcp.tracklution.com/mcp
```

The flag is `--transport http`, **not** `--transport streamable-http` (the
latter currently fails with "Invalid transport type" in Claude Code 1.x).
Confirm with `claude mcp list` — `tracklution` should appear with
`Auth: Bearer token`.

Takes effect on the next `claude` invocation (or the next chat turn in an
active session).

### Codex CLI

Path: `~/.codex/config.toml` (Windows: `%USERPROFILE%\.codex\config.toml`).
Create the file and parent directory if missing. Do not modify other
`[mcp_servers.*]` blocks the user may already have.

Codex's per-server header table is called **`http_headers`** (NOT `headers`
— that's a Codex-vs-everyone-else quirk):

```toml
[mcp_servers.tracklution]
url = "https://mcp.tracklution.com/mcp"
enabled = true

[mcp_servers.tracklution.http_headers]
Authorization = "Bearer <jwt>"
```

Codex also offers a `bearer_token_env_var = "ENV_NAME"` alternative, but it
requires the user to export `$ENV_NAME` in every shell that launches Codex
— don't go there from an agent. The inline `http_headers` table above is
the agent-safe form.

Codex does NOT hot-reload `config.toml`. After writing, tell the user to
restart their Codex CLI session.

**Known caveat (openai/codex#11284):** some Codex builds fail to initialize
Streamable HTTP MCP servers and surface them in `/mcp` with `Tools: (none)`.
If `get_status` returns no tools after the restart, ask the user to upgrade
Codex to the latest release; if that doesn't help, fall back to OAuth for
this host.

### Windsurf

Path: `~/.codeium/windsurf/mcp_config.json` (Windows:
`%USERPROFILE%\.codeium\windsurf\mcp_config.json`). Create parent
directories if needed.

Windsurf accepts both `url` and `serverUrl` for the endpoint field — you can
merge `data.mcp_config_snippet` as-is, or rename `url` → `serverUrl`
(Windsurf's preferred form). Either works.

```json
{
  "mcpServers": {
    "tracklution": {
      "serverUrl": "https://mcp.tracklution.com/mcp",
      "headers": { "Authorization": "Bearer <jwt>" }
    }
  }
}
```

Windsurf does NOT hot-reload. After writing, tell the user to fully restart
Windsurf from the system tray (closing the window is not enough), or click
the Refresh icon in the Cascade MCP toolbar.

### Cline

Path:

- macOS VS Code: `~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json`
- Windows VS Code: `%AppData%\Code\User\globalStorage\saoudrizwan.claude-dev\settings\cline_mcp_settings.json`
- Linux VS Code: `~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json`
- Cline CLI: `~/.cline/data/settings/cline_mcp_settings.json`

Required: Cline v3.17.11 or newer. The `type` field must be `streamableHttp`
(camel case, NOT `http`):

```json
{
  "mcpServers": {
    "tracklution": {
      "type": "streamableHttp",
      "url": "https://mcp.tracklution.com/mcp",
      "headers": { "Authorization": "Bearer <jwt>" },
      "disabled": false
    }
  }
}
```

Open the Cline MCP servers panel and click the refresh icon, or restart VS
Code.

**Known caveat (cline/cline#2926, #3090, PR #8067):** a few Cline builds
between v3.17.11 and the #8067 merge spawn an OAuth provider that overrides
the manual `Authorization` header. If `get_status` returns 401 after the
panel refresh, fall back to OAuth for this host — Cline's OAuth prompt then
handles auth instead.

## Per-host OAuth fallback

Use this when the host can't edit MCP config from the agent (Lovable,
Replit, Bolt), or when the magic-install POST returned
`next_action.tool === "oauth_fallback"`.

### Cursor (file-edit, OAuth fallback)

Write the **tokenless** body into `.cursor/mcp.json` at the project root:

```json
{
  "mcpServers": {
    "tracklution": {
      "url": "https://mcp.tracklution.com/mcp"
    }
  }
}
```

User opens Cursor → Settings → MCP and clicks **Connect** next to
**tracklution**. A browser tab opens; they sign in to their existing
Tracklution account. When the tab closes, they reply `go`.

One-click alternative (if the user prefers not to commit `.cursor/mcp.json`):

```
cursor://anysphere.cursor-deeplink/mcp/install?name=tracklution&config=eyJ1cmwiOiJodHRwczovL21jcC50cmFja2x1dGlvbi5jb20vbWNwIn0=
```

Requires Cursor v0.48.0 or newer.

### Claude Code CLI (CLI, OAuth fallback)

```bash
claude mcp add --transport http tracklution https://mcp.tracklution.com/mcp
```

(No `--header` flag — that triggers OAuth on first tool call.)

### Codex CLI / Windsurf / Cline (file-edit, OAuth fallback)

Write the tokenless body documented above but **omit the `headers` block
entirely** (or the `http_headers` table for Codex). Then tell the user to
restart the host (Codex / Windsurf) or open the host's MCP panel and click
the Connect / Authenticate button (Cline).

### Lovable (user-action)

> To finish installing Tracklution in Lovable: open Lovable Settings →
> Integrations → MCP servers → Add custom MCP server. URL:
> `https://mcp.tracklution.com/mcp`. Lovable will prompt you to sign in to
> Tracklution in a browser tab. Once you're signed in, come back and reply
> `go`. Custom MCP servers are available on paid Lovable plans.

### Replit Agent (user-action)

> To finish installing Tracklution in Replit: go to
> https://replit.com/integrations → MCP Servers → Add MCP server. Name it
> `tracklution`. URL: `https://mcp.tracklution.com/mcp`. Click Test & Save —
> Replit will prompt you to sign in to Tracklution in a browser tab. Once
> signed in, come back and reply `go`.

### Bolt (user-action)

> To finish installing Tracklution in Bolt: open Bolt → Connectors → Add
> custom connector. URL: `https://mcp.tracklution.com/mcp`. Transport: HTTP.
> Bolt will prompt you to sign in to Tracklution in a browser tab. Once
> signed in, come back and reply `go`.

After the user signs in, the host attaches the user's container-scoped JWT
to the MCP entry automatically. Proceed with Turn 4 of the contract.

## Verify and score — completion contract

`verify_and_score` is mutating; pass a fresh `idempotency_key` on every
call. Read `status` and `verification.not_ready_reason` together:

| `status` | `not_ready_reason`                         | Action                                                                                                  |
|----------|--------------------------------------------|---------------------------------------------------------------------------------------------------------|
| `ok`     | `null`                                     | Done. Call `create_login_link(target_page=dashboard)`. Send final hand-off.                              |
| `needs_action` | `awaiting_connector_activation`      | Call `create_login_link(target_page=connectors)`. Surface URL + `verification.message`. DO NOT retry.    |
| `needs_action` | `awaiting_first_party_mode`          | Call `create_login_link(target_page=dns)`. Surface URL + `verification.message`. DO NOT retry.           |
| `needs_action` | `events_processing`                  | Retry per `retry.retry_after_seconds`/`retry.max_retries_recommended`. If still failing, exit happily.   |
| `needs_action` | any event-shape gap                  | Wait for the page load / user to trigger missing event. Retry per the budget.                            |

Event-shape gaps (the `not_ready_reason` enum's "events are still missing"
class):

- `no_events_after_install`
- `event_not_received_yet`
- `script_not_seen`
- `only_pageview_seen`
- `missing_bottom_funnel_event`
- `missing_contact_info`
- `domain_mismatch`

**Critical**: `verification.scoring_complete` MAY remain `false` even on a
fully-functional install because of organic metrics (e.g. recovery-rate-based
scoring) that mature over time. Treat `status: ok` (`not_ready_reason: null`)
as the completion signal, NOT `scoring_complete`.

### `container_hash` — the dual-key auth pattern

Onboarding tools (`get_installation_scripts`,
`select_installation_method`, `verify_and_score`, `get_next_steps`,
`create_login_link`) REQUIRE `container_hash` in their arguments on top of
the `container_id`. The hash is your secret half of a dual-key lookup
against the server-side auth cache; without it the MCP server cannot
dereference the Laravel-side JWT that authorizes onboarding-tool callbacks,
and the tool returns `auth_required` (HTTP 401) even though analytics tools
on the same MCP session work fine.

Where to find the hash:

- **Magic install**: `data.container.hash` in the quick-setup response.
  Save next to `data.container.id`.
- **OAuth fallback**: call `list_containers` after OAuth completes. The
  response shape is `structuredContent.servers[].containers[]`; each
  container carries `id`, `hash`, and `domain`. Match the project's
  website URL against `domain`. Normalize both sides before comparing:
  lowercase, strip the URL's protocol / port / path / query / trailing
  slash, and strip a leading `www.` from both. `domain` may be `null`
  on freshly-created containers — skip those. Then use the matched
  entry's `(id, hash)` pair for every onboarding-tool call.

  Pre-v5 schema bug note: in MCP versions before v0.3.0 (alongside
  agent-install.md v5) the `hash` field was silently stripped from
  `list_containers` and `get_container` outputs because the MCP-side
  zod schema didn't declare it. Agents running against an older MCP
  server will see `containers[].hash === undefined` and must call
  `register_and_provision` with `auth_token` + `website_url` to obtain
  the hash instead. Servers at v0.3.0 or newer forward the field
  natively.
- **`next_action.args`** on any onboarding-tool response: the server seeds
  `container_hash` as a breadcrumb for the next call. Use it verbatim.

## Two-axis model

Tracklution uses two orthogonal enums for the install workflow.

### Axis 1: `framework` — the user's web stack

Strict enum: `html | nextjs`. (The REST endpoint also accepts `other` and
maps it to `html` server-side, but agents should never need that — `html`
is the correct catch-all.)

Even on Shopify / WooCommerce / WordPress sites, pass `framework: html` (or
`nextjs` for a Next.js project). The storefront platform is captured by
`event_source_system` (axis 2), NOT by `framework`. Passing
`framework: shopify` will fail validation with HTTP 422.

### Axis 2: `event_source_system` — the install workflow

Optional. Validated against the `installation_methods` enum:

- `manual` — agent pastes snippets into the HTML head or framework equivalent.
- `gtm` — Google Tag Manager cHTML tags.
- `gtm-template` — prebuilt Tracklution GTM community template.
- `woocommerce` — Tracklution WooCommerce plugin (user installs via WordPress admin).
- `shopify` — official Tracklution Shopify app.
- `stripe` — Stripe webhook to `https://t.<yourdomain>/collect/hook?k=<key>` on `payment_intent.succeeded`.
- `affiliate` — affiliate / postback URL configuration (hand off to user).
- `magic` — experimental auto-detection (not user-facing today).

Set this from `scout_website`'s `suggested_installation_methods[0]` when
present, or from explicit user choice. If unknown, omit it — the platform
will infer `manual`.

### Examples

| Project                                          | `framework` | `event_source_system`  |
|--------------------------------------------------|-------------|------------------------|
| Plain Vite + React site                          | `html`      | (omit)                 |
| Next.js marketing site                           | `nextjs`    | (omit)                 |
| Shopify storefront with `theme.liquid`           | `html`      | `shopify`              |
| WooCommerce store on WordPress                   | `html`      | `woocommerce`          |
| Stripe-only SaaS sending purchase via webhook    | `html`      | `stripe`               |

### `agent_client` enum

Strict: `cursor`, `claude-code`, `windsurf`, `lovable`, `replit`, `bolt`,
`cline`, `codex`, `other`. Any unknown host should fall back to `"other"`.

## Error code table

Every code is sourced from
[`StableErrorCode.php`](https://github.com/tracklution/tracklution-api/blob/main/src/Mcp/Onboarding/Enums/StableErrorCode.php).
Match against codes, not message text.

### Validation / contract errors (fix-then-retry)

- `validation_failed` (HTTP 422) — payload failed Laravel validation.
  `errors.<field>` arrays name the bad fields. Fix and retry.
- `invalid_domain` (422) — `website_url` is malformed or points at a
  reserved/local hostname. Re-prompt.
- `installation_method_invalid` (422) — `event_source_system` is not in the
  `installation_methods` enum. Drop the field or pick a documented value.
- `idempotency_key_required` (400) — every onboarding call needs a fresh
  UUID-v7 `idempotency_key`. Add one.

### Idempotency outcomes (not failures)

- `idempotency_replay` (HTTP 200) — same `idempotency_key` within the 24h
  window. Response is the prior result, replayed safely. Do not retry.
- `idempotency_conflict` (409) — same key, different payload. Either reuse
  the original payload or generate a new `idempotency_key`.

### Conflict (state)

- `duplicate_account` (409) — an account already exists for this email. The
  response carries `next_action.tool === "oauth_fallback"` — follow the
  contract's silent OAuth fallback branch.
- `duplicate_container` (409) — a container already exists for this
  `website_url` under this account. Use it instead of creating a new one.

### Auth (analytics and onboarding)

- `auth_required` (401), `token_expired` (401) — direct the user to the
  OAuth flow per `oauth_resource_metadata_url` in the envelope (the host's
  Connect button).
- `insufficient_permissions` (403), `mfa_required` (403) — surface the
  envelope's `user_instruction` verbatim.

### Throttling and capability

- `rate_limited` (429) — respect `Retry-After` or
  `errors[0].details.retry_after_seconds`. Per-email/per-domain/per-IP/per-agent_client
  caps are separate axes; flipping email will not bypass a domain cap.
- `capability_mismatch` (503) — the MCP and API are out of sync (rare;
  usually during a deploy). Stop, ask the user to retry in a minute.

### Login link state (only emitted from `create_login_link`)

- `login_link_expired` (410) — user took too long. Call `create_login_link`
  again to mint a fresh URL.
- `login_link_already_used` (410) — single-use; if the user actually reached
  the dashboard, install is done. Otherwise mint a new one.

### Server fault (escalate to support)

- `internal_error` (500) — surface the envelope's `correlation_id` to the
  user and tell them to forward to support@tracklution.com.

## Step 4 — analytics and stats after install

After installation the MCP is authenticated for this user/container. If the
user later asks "how many events?", "what are my container stats?", "show
me top sources", or any analytics question, call the appropriate tool
directly — `get_summary`, `get_report`, `list_containers`, `list_events`,
`list_sessions`, `query_events`, `query_sessions`, `get_container`,
`get_api_key_info`, `get_status`. There is no extra OAuth step; the JWT in
the host's MCP config already grants access.

If the JWT has expired (rare — long-lived) the MCP returns 401 +
`WWW-Authenticate` and the host surfaces its Connect button.

`create_login_link` is for one-shot dashboard hand-off (after install or
when the user needs to activate a connector / enable DNS). Do **not** use
it to answer analytics questions.

## Notes on file safety

After magic install, the host's MCP-config file
(`.cursor/mcp.json`, `~/.codex/config.toml`,
`~/.codeium/windsurf/mcp_config.json`, `cline_mcp_settings.json`, or the
Claude Code CLI config managed by `claude mcp add`) **contains a Bearer
JWT** in `headers.Authorization`. This JWT is container-scoped and
revocable from the Tracklution dashboard.

- For a private repo where only the user(s) listed on the Tracklution
  account have access, committing `.cursor/mcp.json` is fine and lets
  teammates pick up the install automatically.
- For a public repo (open-source project, demo, fork), do **not** commit
  the file. Either add `.cursor/mcp.json` to `.gitignore` or have each
  developer install into `~/.cursor/mcp.json` (Cursor's per-user config)
  instead of the project-local one. Same advice applies to the Windsurf
  and Cline config paths.
- The OAuth-fallback path writes a tokenless entry — no Authorization
  header — so committing it to any repo is always safe.

## Reference URLs

- Agent contract (primary doc): <https://www.tracklution.com/agent-install.md>
- Service directory: <https://www.tracklution.com/.well-known/tracklution.json>
- Install recipes (machine-readable): <https://www.tracklution.com/api/install-recipes/>
- MCP server: <https://mcp.tracklution.com/mcp>
- Magic-install REST bootstrap: <https://api.trlution.com/install/quick-setup>
- Reporting API base: <https://api.trlution.com/mcp-api/v1>
- Knowledge base: <https://www.tracklution.com/docs/>
- Support: <support@tracklution.com>

## Changelog

- **v5 (2026-05)** — Agent Install Contract introduced as the primary doc
  surface. Per-host detail, error code table, two-axis model, and
  file-safety notes moved here. `data.scripts` and `data.next_steps`
  dropped from the `/install/quick-setup` response — the canonical source
  for snippets and step guidance is `get_installation_scripts` after the
  MCP is authenticated. New `next_action.tool === "oauth_fallback"`
  convention on `duplicate_account` (HTTP 409). `get_status` output
  schema field renamed `ok` → `reachable` to match doc semantics.
  `errors[0].details.recovery_url` added to the duplicate-account
  envelope.
- **v4 (2026-05)** — Magic install promoted to the primary path. New REST
  bootstrap endpoint `POST https://api.trlution.com/install/quick-setup`
  returns a `mcp_config_snippet` carrying `Authorization: Bearer <jwt>`.
  MCP server no longer exposes public-mode onboarding tools — every tool
  call requires a Bearer JWT. The Connect-button OAuth flow is now the
  fallback (existing accounts, user-action hosts).
- **v3** — Two-axis model (`framework` × `event_source_system`)
  introduced; `codex` added to `agent_client` enum.
- **v2** — Initial MCP-based protocol.
