---
title: "MCP server"
description: "Create conversion goals and funnels from Claude Code or Cursor with a personal API key over /api/v1."
source: "https://xpmetric.com/docs/mcp-server"
---
The XPmetric MCP server is a **stdio** process for Claude Code and Cursor. It talks to the same `/api/v1` surface as the dashboard, while the agent has your checkout (or signup) code open — so it can add `window.xpmetric('event')` and create the matching goal in one turn.

Remote HTTP MCP is not supported.

A goal only fires if `window.xpmetric('event_name')` runs on the page (`p.js` must already be installed). The MCP server **never writes your files** — it returns a snippet; your editor tools paste it.

## Cookieless funnel rule

On cookieless sites (the default) visitor identity resets every UTC day, so visitor scope cannot complete across days. Use session scope, or windowHours ≤ 24, or ask the user to switch the site to cookie mode. Never silently rewrite scope or windowHours.

See [GDPR, cookieless tracking & tracking modes](/docs/gdpr-cookieless).

## Mint an API key

1. Open **Settings → General → API keys**.
2. Name a key and copy it once. The prefix is `xpm_mcp_`.
3. A key covers every site on the account.

Do **not** use a per-site Payments secret (`xpm_sk_…`) here — that path is [POST /api/payment](/docs/revenue/payments-api) only.

Revoke unused keys from the same Settings panel. Cap is 10 active keys per account.

## Install

Set `XPMETRIC_API_KEY` to the raw key. Optional `XPMETRIC_BASE_URL` defaults to `https://xpmetric.com` (use `http://127.0.0.1:3000` against a local app).

### Claude Code

```json
{
  "mcpServers": {
    "xpmetric": {
      "command": "npx",
      "args": ["-y", "@xpmetric/mcp"],
      "env": {
        "XPMETRIC_API_KEY": "xpm_mcp_YOUR_KEY"
      }
    }
  }
}
```

### Cursor

Add the same block under **Cursor Settings → MCP** (or `.cursor/mcp.json`):

```json
{
  "mcpServers": {
    "xpmetric": {
      "command": "npx",
      "args": ["-y", "@xpmetric/mcp"],
      "env": {
        "XPMETRIC_API_KEY": "xpm_mcp_YOUR_KEY"
      }
    }
  }
}
```

Restart the MCP session after saving.

### Local checkout

Until `@xpmetric/mcp` is on npm, build the package in this repo and point the editor at the compiled bin:

```bash
cd packages/mcp
npm install
npm run build
```

```json
{
  "mcpServers": {
    "xpmetric": {
      "command": "node",
      "args": ["/ABS/PATH/TO/xpmetric/packages/mcp/dist/index.js"],
      "env": {
        "XPMETRIC_API_KEY": "xpm_mcp_YOUR_KEY",
        "XPMETRIC_BASE_URL": "http://127.0.0.1:3000"
      }
    }
  }
}
```

## Tools

| Tool | Purpose |
|------|---------|
| `list_sites` | Sites on the key (`id`, `domain`, `trackingMode`) |
| `discover_goal_candidates` | Custom events firing with no goal yet — start here |
| `get_tracking_snippet` | Guarded `window.xpmetric('event')` snippet to paste into the repo |
| `list_goals` | Goals plus completions and CVR for a period |
| `create_goal` | Create a goal (`eventName` must match the track call exactly) |
| `update_goal` / `delete_goal` | Patch or remove a goal by id |
| `list_funnels` | Funnels for a site (duplicate names allowed — use ids) |
| `create_funnel` / `update_funnel` / `delete_funnel` | Funnel CRUD (2–8 steps) |
| `get_funnel_data` | Step counts and drop-off for a period |

## Example: track signups

1. Call `list_sites`, then `discover_goal_candidates` for the site.
2. If `signup` is not already firing, call `get_tracking_snippet` with `eventName: "signup"` and paste the snippet after a successful signup (editor tools — the MCP server does not write files).
3. Call `create_goal` with the same `eventName`. Goals are retroactive: matching custom events already in ClickHouse still count.

```js
if (typeof window.xpmetric === "function") {
  window.xpmetric('signup');
}
```

`eventName` is case-sensitive. Do not use `xpm.track`.

## `/api/v1` reference

Scripts and agents can call the HTTP API directly. The MCP package is a stdio wrapper over these routes.

### Auth

- Header: `Authorization: Bearer xpm_mcp_…`
- A signed-in dashboard session cookie is also accepted (same billing gate as the dashboard).

### Rate limit

Dedicated bucket at **120 requests/minute** per user — separate from the dashboard so an agent burst cannot starve an open UI.

Every response includes:

| Header | Meaning |
|--------|---------|
| `X-RateLimit-Limit` | 120 |
| `X-RateLimit-Remaining` | Remaining in the current window |
| `X-RateLimit-Reset` | Unix timestamp (seconds) when the window rolls |

`429` body: `{ "error": "Too many requests" }` plus `Retry-After` (seconds).

### Errors

| Status | Shape |
|--------|--------|
| 401 | `{ "error": "Unauthorized" }` |
| 403 | `{ "error": "Subscription required. …" }` when trial/grace has ended |
| 400 | `{ "error": "Invalid body", "details": { "formErrors": [], "fieldErrors": { … } } }` (Zod `flatten()`) |
| 400 | Cookieless × visitor scope × `windowHours` > 24 — same `details.fieldErrors` on `scope` and `windowHours` |
| 409 | `{ "error": "A goal for this event already exists" }` — `(siteId, eventName)` is unique |

### Routes

| Method | Path |
|--------|------|
| GET | `/api/v1/sites` |
| GET / POST | `/api/v1/sites/{siteId}/goals` |
| PATCH / DELETE | `/api/v1/sites/{siteId}/goals/{goalId}` |
| GET | `/api/v1/sites/{siteId}/goals/discover` |
| GET / POST | `/api/v1/sites/{siteId}/funnels` |
| PATCH / DELETE | `/api/v1/sites/{siteId}/funnels/{funnelId}` |
| GET | `/api/v1/sites/{siteId}/funnels/{funnelId}/data` |

Request bodies match the dashboard: goal `name` + `eventName`; funnel `name`, `steps` (pageview `{ path, match?, hostname?, label? }` or event `{ eventName, label? }`), optional `windowHours` and `scope`. On cookieless sites, omitting `scope` stores **`session`**.

## Related

- [Custom goals](/docs/custom-goals) — the dashboard path for the same events
- [Conversion funnels](/docs/conversion-funnels) — URL matching and visitor vs session
- [Get started](/docs/getting-started) — install `p.js` first
