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
- Open Settings → General → API keys.
- Name a key and copy it once. The prefix is
xpm_mcp_. - 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
| 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
- Call
list_sites, thendiscover_goal_candidatesfor the site. - If
signupis not already firing, callget_tracking_snippetwitheventName: "signup"and paste the snippet after a successful signup (editor tools — the MCP server does not write files). - Call
create_goalwith the sameeventName. 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:
| 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 — the dashboard path for the same events
- Conversion funnels — URL matching and visitor vs session
- Get started — install
p.jsfirst