MCP server

Create conversion goals and funnels from Claude Code or Cursor with a personal API key over /api/v1.

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.

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 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

{
  "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):

{
  "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:

cd packages/mcp
npm install
npm run build
{
  "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

ToolPurpose
list_sitesSites on the key (id, domain, trackingMode)
discover_goal_candidatesCustom events firing with no goal yet — start here
get_tracking_snippetGuarded window.xpmetric('event') snippet to paste into the repo
list_goalsGoals plus completions and CVR for a period
create_goalCreate a goal (eventName must match the track call exactly)
update_goal / delete_goalPatch or remove a goal by id
list_funnelsFunnels for a site (duplicate names allowed — use ids)
create_funnel / update_funnel / delete_funnelFunnel CRUD (2–8 steps)
get_funnel_dataStep 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.
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:

HeaderMeaning
X-RateLimit-Limit120
X-RateLimit-RemainingRemaining in the current window
X-RateLimit-ResetUnix timestamp (seconds) when the window rolls

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

Errors

StatusShape
401{ "error": "Unauthorized" }
403{ "error": "Subscription required. …" } when trial/grace has ended
400{ "error": "Invalid body", "details": { "formErrors": [], "fieldErrors": { … } } } (Zod flatten())
400Cookieless × 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

MethodPath
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.