Part of epic #8286 (Phase 2 — Analytics). Prerequisite for #8606.
Problem
The remote MCP server emits an event named $mcp_tool_call, but populates it with LoopOver's own snake_case property names (tool, ok, duration_ms, arguments, result) from buildMcpToolCallProperties in @loopover/contract.
PostHog's built-in MCP Analytics dashboards read the reserved $mcp_* names literally — $mcp_tool_name, $mcp_duration_ms, $mcp_is_error, $mcp_parameters, $mcp_response. The event therefore ingests cleanly and every breakdown on it renders empty. Nothing surfaces the mismatch.
Two canonical events are also missing entirely:
$mcp_initialize — the handshake, and the only event that says which client connected (Claude Code vs Cursor vs a raw SDK script) and at what version.
$mcp_tools_list — discovery. Joined against $mcp_tool_call on $session_id, it is the difference between a tool nobody wants and a tool nobody could find.
There is no $session_id on any of them, so no cross-event funnel is possible.
Why now
The loopover PostHog project currently holds zero $mcp_tool_call events (POSTHOG_API_KEY was never set on the loopover-api Worker), so the wire shape can be corrected with no historical data to migrate or reconcile. That stops being true the moment the key is set.
Deliverables
Boundaries
Dashboards and insights are #8606. Identity stays anonymous — this is fleet telemetry with no per-actor distinct_id (#6228), and that does not change here.
Resources
Part of epic #8286 (Phase 2 — Analytics). Prerequisite for #8606.
Problem
The remote MCP server emits an event named
$mcp_tool_call, but populates it with LoopOver's own snake_case property names (tool,ok,duration_ms,arguments,result) frombuildMcpToolCallPropertiesin@loopover/contract.PostHog's built-in MCP Analytics dashboards read the reserved
$mcp_*names literally —$mcp_tool_name,$mcp_duration_ms,$mcp_is_error,$mcp_parameters,$mcp_response. The event therefore ingests cleanly and every breakdown on it renders empty. Nothing surfaces the mismatch.Two canonical events are also missing entirely:
$mcp_initialize— the handshake, and the only event that says which client connected (Claude Code vs Cursor vs a raw SDK script) and at what version.$mcp_tools_list— discovery. Joined against$mcp_tool_callon$session_id, it is the difference between a tool nobody wants and a tool nobody could find.There is no
$session_idon any of them, so no cross-event funnel is possible.Why now
The
loopoverPostHog project currently holds zero$mcp_tool_callevents (POSTHOG_API_KEYwas never set on theloopover-apiWorker), so the wire shape can be corrected with no historical data to migrate or reconcile. That stops being true the moment the key is set.Deliverables
$mcp_tool_callcarries PostHog's canonical$mcp_*properties. LoopOver's ownsurface/transport/categorydimensions ride alongside them (PostHog's custom-server docs sanction extra properties explicitly) — they have no canonical equivalent and a breakdown by them would otherwise be impossible.usage_eventis unchanged. It stays LoopOver's own minimal, payload-free event in its own vocabulary.$mcp_initialize, sourced from theinitializerequest's ownclientInfo— not thex-loopover-mcp-*headers, which only our published client sets and which would collapse every third-party client into one bucket.$mcp_tools_list, reporting what this server registered rather than the cross-server contract registry.$session_id(fromMcp-Session-Id),$mcp_source, and$mcp_server_*on every canonical event.MCP_TELEMETRY_ERROR_CODESset onto PostHog's closed$mcp_error_typeset, so their error breakdown populates without losing the precise LoopOver code onusage_event.Boundaries
Dashboards and insights are #8606. Identity stays anonymous — this is fleet telemetry with no per-actor
distinct_id(#6228), and that does not change here.Resources
src/usage-telemetry.ts