Guide · Product Analytics
MCP Tools for Heap Analytics — Server-side events, identity linking, retroactive model, and health monitoring
Heap Analytics is built around a retroactive data model: Heap's browser SDK auto-captures every click, form submission, and page view without requiring explicit event instrumentation. MCP tools integrate with Heap's server-side HTTP API (POST https://heapanalytics.com/api/track) to add backend events (tool calls, API responses, payment confirmations) that the browser auto-capture cannot see. The server-side API accepts events with no response body or error detail — a successful ingestion returns HTTP 204 with an empty body. There is no explicit confirmation of how many events were processed. The most important pattern for MCP tools: server-side events and browser auto-capture events are linked by the identity field, which must match the browser session's heap.identify() call exactly. Without this linkage, server and browser events appear as completely separate users in Heap's session and funnel analysis.
TL;DR
Server-side track: POST https://heapanalytics.com/api/track with JSON body {"app_id": "xxx", "identity": "user@email.com", "event": "Tool Called", "properties": {...}, "timestamp": "ISO8601"}. Identify (link user identity): POST https://heapanalytics.com/api/identify with {"app_id": "xxx", "identity": "user@email.com", "properties": {...}}. Add user properties: POST https://heapanalytics.com/api/add_user_properties. Response: HTTP 200 (empty body) on success — no JSON response, no events_ingested count, no error detail. Auth: app_id in request body (not a header). No official Node.js SDK — use direct HTTP.
Heap's retroactive model and the server-side API's role
Heap's architecture is unusual among analytics platforms: the browser SDK records everything automatically, and you define which captured actions are meaningful after the fact using Heap's Data Manager ("define event" from autocaptured interactions). This retroactive labeling means past data is enriched when you define new events — you don't lose historical data for events you didn't plan to track.
Server-side events from MCP tools occupy a different position in this model. They are explicitly-tracked events that Heap cannot auto-capture (since they happen on the server). These events appear in Heap alongside the autocaptured browser events when linked by identity. Key constraints on server-side events:
- Server-side events appear in Heap but cannot be "defined" from autocapture — they are always explicitly named events.
- Server-side events do NOT carry session information (
pageview_countis always 0 for server-side sessions). Heap funnels that mix server-side and browser events must handle this asymmetry. - The
app_idin the request body is your Heap environment ID (found in Account → Manage → Projects). Unlike other platforms, Heap does not use an API key or Bearer token for the ingestion API — the app_id alone authenticates writes.
// Heap HTTP API client — no official SDK, direct HTTP only
const HEAP_API_BASE = 'https://heapanalytics.com/api';
async function heapRequest(path, body) {
const res = await fetch(`${HEAP_API_BASE}${path}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(10_000),
});
// Heap returns 200 with empty body on success for most endpoints
// It returns 400 with {"error": "..."} on validation failures
if (res.status === 400) {
const text = await res.text().catch(() => '');
throw new Error(`Heap API 400: ${text}`);
}
if (!res.ok) {
throw new Error(`Heap API ${path} → ${res.status}`);
}
// 200 with empty body is success — don't try to JSON.parse()
return { accepted: true, status: res.status };
}
// Track a server-side event
async function trackEvent(appId, identity, eventName, properties = {}, timestamp = null) {
return heapRequest('/track', {
app_id: appId,
identity, // must match heap.identify() in browser SDK
event: eventName,
properties: {
...properties,
// Heap standard properties you can set server-side:
// $browser — Heap uses this for session grouping (set to 'server' for clarity)
// $platform — 'server'
// $ip — Heap uses for geo-enrichment if provided
},
timestamp: timestamp ?? new Date().toISOString(), // ISO 8601 format
});
}
// Link server-side identity to browser session identity
// Call this when a server-side identity maps to a known browser session handle
async function identify(appId, serverIdentity, properties = {}) {
return heapRequest('/identify', {
app_id: appId,
identity: serverIdentity,
// properties here become user-level traits (equivalent to People/Traits in other platforms)
properties,
});
}
// Add or update user-level properties without an event
async function addUserProperties(appId, identity, properties) {
return heapRequest('/add_user_properties', {
app_id: appId,
identity,
properties,
});
}
Identity linking: connecting MCP server events to browser sessions
The most critical integration challenge with Heap for MCP tools is identity linking. Heap assigns every browser session an anonymous numeric ID (visible as window.heap.userId in the browser). When a user logs in, the browser SDK calls heap.identify('user@email.com') to link that anonymous ID to the email. Server-side events must use the same identity string — if you send server-side events with a user ID ("user-123") but the browser identifies with an email ("user@email.com"), Heap treats them as different users.
// Pattern: consistent identity across browser and server
// Browser: heap.identify(user.email) ← must match server-side identity
// Server: trackEvent(appId, user.email, 'Tool Called', {...})
// If you identify by userId in the browser: heap.identify(user.id)
// Then: trackEvent(appId, String(user.id), 'Tool Called', {...})
// For pre-authentication events, Heap's browser SDK assigns an anonymous ID
// The server has no way to know this ID — pre-auth server events appear as
// separate users until the browser calls heap.identify() post-login
// Batch tracking multiple events for one user in a single HTTP call
async function trackEventsBatch(appId, identity, events) {
// Heap does not support batch event ingestion in a single HTTP request
// Each event must be sent individually — for high-volume MCP servers,
// queue events and send them concurrently with rate limiting
const CONCURRENT_LIMIT = 10; // Heap's server-side rate limit is not published
const results = [];
for (let i = 0; i < events.length; i += CONCURRENT_LIMIT) {
const batch = events.slice(i, i + CONCURRENT_LIMIT);
const batchResults = await Promise.allSettled(
batch.map(e => trackEvent(appId, identity, e.event, e.properties, e.timestamp))
);
results.push(...batchResults);
// Small delay between batches to avoid rate limiting
if (i + CONCURRENT_LIMIT < events.length) {
await new Promise(r => setTimeout(r, 100));
}
}
return results;
}
// Track MCP tool calls with Heap-specific patterns
async function trackMcpToolCall(appId, userEmail, toolName, result, durationMs) {
await trackEvent(appId, userEmail, 'MCP Tool Called', {
tool_name: toolName,
success: result.success,
error_type: result.error?.type ?? null,
duration_ms: durationMs,
// Heap auto-captures browser tool interactions; server-side captures
// the actual tool execution with server-only data (duration, errors, costs)
});
}
Retroactive event definitions and Data Manager
Server-side events sent via the API appear in Heap's autocapture event stream with type custom. They can be used in funnels, segments, and retention analysis alongside autocaptured browser events. However, they cannot be "defined" from autocapture — they are already named, with the exact name you passed in the event field. This is actually an advantage for MCP tool events: the event name is stable and programmatic, not dependent on a UI selector that could change.
Heap's Data Manager lets you apply retroactive event labels to autocaptured data. For server-side events, this is unnecessary — the event name is the label. What Data Manager does provide for MCP tool events: property transformations (e.g., marking a property as a metric for use in retention charts) and event groupings (e.g., combining "MCP Tool Called" with "Tool Executed" autocapture events into a unified "Tool Usage" group event).
// Properties that work well with Heap's analysis features:
// - Numeric properties: shown as averages, percentiles in funnels (e.g., duration_ms)
// - Boolean properties: shown as yes/no splits in cohort definitions
// - String with low cardinality: shown as group-by dimensions (e.g., tool_name, plan)
// - String with high cardinality: works but avoid — creates many unanalyzable segments
// Heap's recommended property naming: snake_case, lowercase, no $ prefix
// ($ prefix is reserved for Heap system properties: $browser, $platform, $ip, etc.)
function buildToolCallProperties(toolName, inputTokens, outputTokens, durationMs, errorType) {
return {
tool_name: toolName, // low-cardinality: great for group-by
input_tokens: inputTokens, // numeric: Heap will show average/p95
output_tokens: outputTokens, // numeric
duration_ms: durationMs, // numeric
success: errorType === null, // boolean: yes/no split in cohorts
error_type: errorType ?? 'none', // low-cardinality: group-by dimensions
// DO NOT include: userId, sessionId, requestId (high-cardinality strings
// that create per-row segments instead of meaningful group-by dimensions)
};
}
Health monitoring for Heap integrations
Heap's server-side API provides minimal feedback. A 200 response confirms the HTTP request was received. It does not confirm how many events were queued internally, whether the events will appear in Heap's dashboard within a normal processing window, or whether a property schema issue will cause silent data loss. Heap's processing delay for server-side events is typically under 30 seconds but is not guaranteed.
async function probeHeap(appId) {
const results = await Promise.allSettled([
// 1. Track endpoint availability
(async () => {
const start = Date.now();
const result = await heapRequest('/track', {
app_id: appId,
identity: '__alivemcp_probe__',
event: '__health_probe__',
properties: { synthetic: true, probe_at: new Date().toISOString() },
timestamp: new Date().toISOString(),
});
const latencyMs = Date.now() - start;
return { kind: 'track', accepted: true, latencyMs };
})(),
// 2. Identify endpoint availability
(async () => {
const result = await heapRequest('/identify', {
app_id: appId,
identity: '__alivemcp_probe__',
properties: { is_probe: true },
});
return { kind: 'identify', accepted: true };
})(),
// 3. add_user_properties endpoint availability
(async () => {
const result = await heapRequest('/add_user_properties', {
app_id: appId,
identity: '__alivemcp_probe__',
properties: { last_probe: new Date().toISOString() },
});
return { kind: 'user_properties', accepted: true };
})(),
]);
const healthy = results.every(r => r.status === 'fulfilled');
return {
healthy,
// Caveats: healthy=true means HTTP 200 received, NOT confirmed event processing.
// Heap does not expose a query API to verify events appeared in the dashboard.
// For end-to-end verification, check Heap's Live View in the dashboard manually
// or use Heap's Webhooks/Data Export to a warehouse you can query.
components: results.map(r =>
r.status === 'fulfilled'
? { ...r.value, ok: true }
: { ok: false, error: r.reason?.message }
),
};
}
Set AliveMCP alerts: critical when any Heap endpoint returns 400 (bad app_id, malformed event — check the error body for the specific validation failure) or 5xx; warning when the track endpoint latency exceeds 2 seconds (Heap's API is normally sub-200ms — high latency indicates ingestion backpressure); info when 400 error bodies mention property type mismatches (indicates a code change sent an unexpected property type — e.g., string where Heap expects numeric).
Common integration pitfalls
- Identity mismatch between browser and server
- The identity string in server-side API calls must exactly match what the browser SDK passes to
heap.identify(). If the browser identifies with the user's email address and the server sends events with a numeric user ID, Heap creates two separate user profiles that are never merged. Establish a single canonical identity string (email, user ID, or UUID) and use it consistently in both browser and server contexts. - Expecting a response body from the API
- Heap's server-side API returns HTTP 200 with an empty body on success. Do not attempt to parse a JSON response — there is none. The only structured response is on 400 errors, which return a JSON body with an
errorstring. Treating an empty 200 body as a parse error causes false negatives in your health probes. - Sending sessions or pageview_count from server-side events
- Server-side events do not have browser context. Properties like
pageview_count,session_id, or$current_urlthat Heap auto-captures for browser events are meaningless for server-side events — and Heap's session analysis excludes server-side events from session counts entirely. Do not attempt to synthesize browser session properties on server-side events; it confuses Heap's session stitching logic. - Using high-cardinality property values as group-by dimensions
- Heap's analysis UI lets you group events by property values. High-cardinality properties (user IDs, request IDs, full error messages, arbitrary strings) produce unworkable group-by dimensions with thousands of unique values. Normalize high-cardinality data into categorical buckets (error_type instead of error_message, tool_category instead of exact tool name) before sending to Heap.
- Sending events without an identity for server-side sessions
- If your MCP server processes unauthenticated requests and you send events with
identity: nullor an empty string, Heap creates anonymous user profiles that accumulate as noise and cannot be linked to any browser session or identified user. Either require authentication before capturing server-side events, or use a stable anonymous session token and accept that these events will appear as unidentified users in Heap.
Related guides
- MCP tools for PostHog — event capture, feature flags, HogQL query API, and pipeline health
- MCP tools for Amplitude — event tracking, SDK vs HTTP API, 429 throttle sub-cases, and health probes
- MCP tools for Mixpanel — distinct_id model, People profiles, EU data residency, and ingestion health
- MCP tools for Segment — identify/track/group calls, Destination delivery opacity, and Protocols enforcement
- MCP server health check patterns — composite endpoints and failure classification
- MCP server uptime monitoring — probes, intervals, and alert routing