Debugging MCP analytics
SkillDatabases & dataDebug, support, and build PostHog MCP Analytics — product analytics for MCP servers (the `@posthog/mcp` and `posthog.mcp` SDKs plus the mcp_analytics product). Use when MCP analytics data looks wrong or missing ("events aren't showing", "intent clusters are empty", "sessions are missing", "per-tool numbers look wrong"), when writing queries over `$mcp_*` events by hand, or when doing feature work on the SDKs, the dashboard and its query runners, the self-instrumented MCP server, the `wizard mcp-analytics` install command, or the in-app onboarding. Covers the repo map, the `$mcp_*` vocabulary and where each property comes from, the rules that silently corrupt metrics when ignored, the end-to-end pipeline and where each stage breaks, and which repo to change. For reading the data rather than fixing it, prefer the `exploring-mcp-*` and `improving-mcp-tools` skills.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Debugging MCP analytics skill
What this skill tells your AI
The instructions your AI receives, as published by posthog/posthog in .agents/skills/debugging-mcp-analytics/SKILL.md and read by ahel’s review.
Product analytics for MCP servers. A team ships an MCP server; the @posthog/mcp SDK
wraps it in one line; every tool call, agent intent, and failure lands in PostHog as a
$mcp_* event you can query, chart, alert on, and cluster — plus a dedicated dashboard. The
MCP-layer sibling of @posthog/ai.
The differentiator is intent: not "ran query_run 14 times" but "was trying to find a
churn cohort". Explicit non-goal: this does not replace LLM analytics / AI observability
— generation traces, prompt/response, and token cost belong there.
Status: beta, TypeScript and Python SDKs shipped, whole product still behind the
mcp-analytics early-access flag (products/mcp_analytics/frontend/featurePreviewGate.ts).
PostHog dogfoods it — its own MCP server instruments itself, and that data drives the
dashboard. Public tracking: mega-issue PostHog/posthog#64016, which is the live source
for roadmap and customer wishlist.
Repos
GitHub is the source of truth for where the code lives. Paths below are in-repo; for the repos outside this monorepo, resolve a local checkout via references/local-repos.md rather than assuming a location.
| Concern | Repo | Where to look |
|---|---|---|
| Product / dashboard | PostHog/posthog (this repo) | products/mcp_analytics/ — Django/DRF + HogQL query runners + Temporal, Kea frontend, the query-mcp-* tool registry, and the analysis skills |
| Self-instrumented server | PostHog/posthog (this repo) | services/mcp/ — PostHog's own MCP server (Hono); the dogfood event producer. Also hosts the generated query-mcp-* handlers |
| Shared query reference | PostHog/posthog (this repo) | models-mcp.md — products/posthog_ai/skills/querying-posthog-data/references/ |
TypeScript SDK @posthog/mcp | PostHog/posthog-js | packages/mcp/ — the library customers install. Vocabulary source of truth: src/extensions/constants.ts. docs/ARCHITECTURE.md now covers conversation anchoring (ADR-0004) but trails the newest era handling — where it and CHANGELOG.md disagree, trust the changelog and the source |
Python SDK posthog.mcp | PostHog/posthog-python | posthog/mcp/ — mirrors posthog.ai. Ships inside posthog (pip install posthog); mcp/fastmcp are lazily-imported peer deps, no [mcp] extra. At TS parity since 7.40.0-7.42.1 — MCP Python SDK v2, conversation anchoring, typed errors, client UA/vendor |
| Docs | PostHog/posthog.com | contents/docs/mcp-analytics/ (incl. surfaces/), plus src/hooks/productData/mcp_analytics.tsx and the mcp_analytics entry in src/data/tools.ts |
| Install codemod | PostHog/context-mill | context/skills/mcp-analytics/{config.yaml,description.md} |
| Wizard CLI | PostHog/wizard | bin.ts, src/commands/mcp-analytics.ts, src/lib/programs/mcp-analytics/ |
| Wizard test harness | PostHog/wizard-workbench | apps/mcp-analytics/ fixtures |
Don't conflate:
PostHog/mcp-analyticsis the archived prototype of this SDK — stuck at0.0.9with an oldtrack(server, {...})API. It published under the same@posthog/mcpname, so grepping that name can land you there. npm@posthog/mcpnow resolves toPostHog/posthog-js.products/mcp_store/is the MCP server marketplace / team gateway, not this product. (Older notes also mention aproducts/mcp/build-tooling directory; it no longer exists — the server and its generation tooling live inservices/mcp/.)wizard mcp addinstalls the PostHog MCP server into a coding agent. That is NOTwizard mcp-analytics, which instruments the user's own server.
Line numbers drift and this area moves fast — grep for the symbol, never trust a remembered line number. Confirm a checkout is on a sane branch before quoting its code.
Hard rules (break these and the numbers are silently wrong)
These are the failure modes that produce a plausible-looking answer rather than an error.
- Always resolve the effective tool name through
EFFECTIVE_TOOL_SQL. The expression lives once, inproducts/mcp_analytics/backend/hogql_queries/base.py:coalesce(nullIf(toString(properties.$mcp_exec_tool_call_name), ''), toString(properties.$mcp_tool_name)). It exists because a single-exec server can report the tool two different ways, and the two eras of data coexist. Todayservices/mcpresolves the inner tool itself and passes it straight in as the tool name (execToolName()insrc/hono/tool-executor.ts, which falls back to the literalexecwhen the inner command isn't recognized), so$mcp_tool_nameusually already holds the real tool.$mcp_exec_tool_call_nameis registered inposthog/taxonomy/taxonomy.pyand coalesced defensively here, but nothing on master emits it — treat it as historical rows plus in-flight work, not current producer behaviour. Either way, aggregate through the coalesce: hand-rollingproperties.$mcp_tool_namealone silently buckets unrecognized exec calls underexec, and misses any data that does carry the dedicated property. - Failures come from
$mcp_is_error/$mcp_error_type/$mcp_error_status, never$exception.$exceptioncan be disabled, isn't emitted when no error value is passed, and never matched new-SDK events — so querying it returns nothing rather than failing. - Dash the in-progress bucket. Every time-bucketed chart zero-fills and marks the final
incomplete interval via
products/mcp_analytics/frontend/timeBuckets.ts(resolveWindow,normalizeBucket,buildBucketKeys,lastBucketIsInProgress). Omit it and a partial period reads as a real decline. harnessis derived, and its logic exists in three places that must move in lockstep:products/mcp_analytics/backend/mcp_harness.py(source of truth — see its module docstring),products/mcp_analytics/frontend/dashboard/harnessRegistry.ts, andmodels-mcp.md.- Check which SDK version the dogfood server is on before trusting dogfood data.
services/mcpconsumes the SDK through an alias in itspackage.jsonand has historically lagged the published version, so version-dependent properties (typed error types,$libidentity, payload redaction) can be absent from PostHog's own data even when documented as current. A query filtering on$lib = 'posthog-node-mcp'silently excludes all dogfood traffic if that pin predates SDK 0.7.0. Note too thatservices/mcpuses the custom-dispatcher (PostHogMCP) path rather thaninstrument(), so behaviour living only in theinstrument()path — stable sessions,$identifydeduplication,_meta-based client identity — has never applied to it at any version. - Know which session model produced the data. Under the stateless spec there is no
transport session, so
$session_idis only stable if the server opted into conversation anchoring —enableConversationId, which is off by default. With it off, a stateless client's sessions fragment (often one per request); with it on,$session_idis derived from an agent-echoed handle and survives reconnects, restarts, and pods. Check the flag before diagnosing "fragmented sessions" as an ingestion problem. See references/stateless-and-sessions.md. - There are no SQL template files. Every dashboard and tool-quality query is a typed
query runner behind the generic
/query/endpoint. Abackend/templates/*.sqlreferenced by older notes no longer exists.
Event vocabulary
All data lives on the shared ClickHouse events table — there is no dedicated table.
Every metric is an aggregation over $mcp_tool_call, usually grouped by $session_id.
Source of truth for the SDK-emitted names is packages/mcp/src/extensions/constants.ts in
PostHog/posthog-js, exported as PostHogMCPAnalyticsEvent / PostHogMCPAnalyticsProperty
(import them for typesafe queries). PostHog-side descriptions — including the server-stamped
and exec-mode properties the SDK does not define — live in posthog/taxonomy/taxonomy.py.
Events (all $-prefixed; non-$ names would be treated as customer events):
$mcp_tool_call (primary), $mcp_tools_list, $mcp_initialize, $mcp_missing_capability,
$mcp_resource_read / $mcp_resources_list, $mcp_prompt_get / $mcp_prompts_list,
$identify, $exception.
$mcp_initializeis not a reliable session anchor — but check whose server you're looking at. The 2026-07-28 revision removes theinitializehandshake, so a customer server on the SDK'sinstrument()path emits nothing for a stateless client. PostHog's own server is the exception:services/mcpfires the same$mcp_initializeevent fromserver/discoveras frominitialize(dispatcher.ts::recordDiscoveryRequestcovers both entry points), so the event is present in dogfood data either way. Treat its absence as meaningful only for customer servers. The real anchor is now the conversation handle when the server enables it — references/stateless-and-sessions.md covers the resolution order and the delivery protocol. Live consequence, for customer servers only:frontend/mcpAnalyticsOnboardingLogic.tsderiveshas_initializefrom this event, so a stateless customer server reads asnot-instrumenteduntil its first tool call. Onboarding still completes —hasToolCallis checked first, in both that selector andstatusFromProbeDefinitions. Projects onservices/mcpare unaffected, since it emits the event fromserver/discover.
Full property tables — split by provenance (SDK-emitted vs stamped by PostHog's own server vs exec-mode only), the identifier distinctions, per-version SDK behaviour, and TypeScript/Python parity — are in references/event-vocabulary.md. Read that before writing queries or changing what gets captured.
Reading the data
Governed metric first
When debugging an MCP failure-rate headline, call posthog:metric-list before the dedicated analysis skills, typed tools, or hand-written HogQL and look for mcp_tool_call_fail_pct. Run an approved, non-drifted match with posthog:data-catalog-metric-run as the canonical headline. Use the paths below only for requested tool, harness, or time breakdowns after that run, and label those breakdowns noncanonical. If no governed metric matches, state that the catalog has no match and label the derived rate noncanonical.
Prefer the dedicated analysis skills over hand-written HogQL; they already encode the exec-mode and harness handling that Hard rules 1 and 4 describe:
exploring-mcp-tool-usage— front door / router: takes a broad "how is my MCP doing" question and dispatches to the right typed tool or focused skill. Start here.exploring-mcp-tool-quality— error rates, latency, reach, failing and slow tools.exploring-mcp-sessions— session list, per-session tool calls, intent.exploring-mcp-intent-clusters— "what are people trying to do" clusters.improving-mcp-tools— eval-scored campaign loop: measure, make one bounded fix, re-measure.
Typed tools exist for most questions and are preferable to raw SQL: posthog:query-mcp-tool-stats,
-daily-stats, -failures, -failure-occurrences, -descriptions, -neighbors,
-sample-intents, -top-users, and posthog:query-mcp-harness-breakdown, plus session tools
(posthog:mcp-analytics-sessions-list / -tool-calls / -generate-intent) and the intent-cluster
tools. They are declared in products/mcp_analytics/mcp/tools.yaml.
Harness is the friendly label for the calling client (Claude Code, Cursor, ChatGPT,
Windsurf, and ~30 other buckets). It is resolved at query time only, with no stored column:
mcp_harness.py::HARNESS_TOKEN_SQL picks the strongest available signal in priority order,
over exactly three properties — the ones the SDK schemas can emit
($mcp_vendor_client, with the legacy non-$ mcp_vendor_client coalesced for historical
rows -> Claude Code user-agent surface -> Grok user-agent -> $mcp_client_name -> generic
user-agent token, both from $mcp_client_user_agent), then
harness_label_sql() buckets it (or harness_label_or_token_sql(), which names an
unrecognized client verbatim instead of collapsing it into "Other" — use it for ranked
top-N lists, never where labels feed an array or unbounded GROUP BY).
$mcp_client_name is one mid-priority input, not a synonym for harness — grouping by
it directly gives a different, messier answer: on old SDK versions it rode only on the
session's initialize, and Anthropic's pooled surfaces self-report a generic
Anthropic/ClaudeAI that only the vendor header can disambiguate. The dogfood-only
mcp_session_client_name and $mcp_oauth_client_name are no longer read by harness
resolution — the server folds the session-pinned name into per-event $mcp_client_name,
and neither property ever resolved an event alone.
For hand-written SQL, models-mcp.md
carries the property reference and worked query examples.
The pipeline, and where each stage breaks
- Instrument -> the server emits
$mcp_*events via the SDK. Breaks: handlers not wrapped (instrument()is idempotent and degrades to a silent no-op on failure); a STDIO server writing to stdout withconsole.*(corrupts the protocol stream — wire alogger); a disabled or misconfigured posthog-node client. Forservices/mcpthere is a single emission path:src/hono/analytics.ts+src/hono/tool-executor.ts->getPostHogClient()(src/lib/posthog/client.ts) ->PostHogMCP, consumed through the dependency alias@posthog/mcp-analytics(the alias matters when grepping imports). The legacy MCPcat/AgentCat shim and the transition shim that dual-emitted non-$mcp_tool_call/mcp_initializewere both removed and are regression-tested inservices/mcp/tests/hono/.services/mcp/ARCHITECTURE.mdstill describes the old multi-emitter design and references a deletedlib/mcpcat.ts— trust the source, not that document. - Ingest -> events land in ClickHouse
events. Breaks: ordinary ingestion and quota problems;$session_idnot materialized, which breaks session grouping. - Session list ->
backend/logic.py::list_mcp_sessionsruns HogQL over a 7-day default window (DEFAULT_SESSIONS_DATE_FROM, resolved throughQueryDateRangewith a one-day overlap buffer each side) and caches for 30s (SESSIONS_CACHE_TTL_SECONDS). Breaks: anything outside the window simply isn't there; results can be up to 30s stale. - Charts and tool quality -> typed
AnalyticsQueryRunnersubclasses inbackend/hogql_queries/(base.py,dashboard_series.py,harness_breakdown.py,tool_quality_tables.py,tool_tables.py), dispatched via the generic/query/endpoint and enumerated inbackend/facade/queries.py, with schemas inposthog/schema.py. Gate:hogql_queries/base.py::validate_mcp_analytics_access— the feature flag plus themcp_analyticsRBAC resource. Breaks: flag off, RBAC denies, or Hard rules 1-3 ignored. - Intent generation (on demand, per session) -> collect
$mcp_intentvalues -> an LLM summary of at most two sentences -> Postgresposthog_mcp_session. A second, project-level path produces the intent digest / themes with structured output, bounded byMAX_DIGEST_THEMES;resolve_themes()derives every countable field from the corpus so the model cannot invent numbers. Model constants live inbackend/intent_generation.py. Breaks: no$mcp_intentcaptured at all (the agent never filled the injectedcontextargument and nointentFallbackwas configured), so there is nothing to summarize; LLM key or quota problems. - Intent clustering (behind
mcp-analytics-intent-routing) -> embed (cached inMCPIntentEmbeddingCache) -> agglomerative clustering (cosine, average linkage,DEFAULT_DISTANCE_THRESHOLD) -> JSONBMCPIntentClusterSnapshot. Temporal end-to-end, no Celery. On-demand recompute (trigger_intent_cluster_recompute, serialized withselect_for_update()and a deterministic per-team workflow id) and thecluster_mcp_intentsmanagement command both start the workflow; the daily run is a Temporal Schedule (posthog/temporal/mcp_analytics/intent_clustering/schedule.py, behind themcp-analytics-clustering-scheduleflag) that triggersIntentClusteringCoordinatorWorkflow, which fans out one child workflow per team. Two caps will surprise you:MAX_SNAPSHOT_CLUSTERS(snapshots keep only the top clusters by volume, enforced at write and again at read) andMAX_QUERY_ROWS. Note the corpus does not depend on step 5:fetch_intent_corpustakes each session's first$mcp_intentstraight from ClickHouse and only overrides it with the stored LLM summary where one exists. So a project can cluster with no generated summaries at all. Breaks: empty clusters almost always mean no$mcp_intentvalues in the lookback window (check the corpus before chasing summary generation); schedule flag off; stale embeddings. Also check the allowlist —intent_clustering/team_discovery.pycurrently returns a hard-codedGUARANTEED_TEAM_IDS = [2], so the daily schedule covers only PostHog's own project and enabling the flag elsewhere still produces nothing until that changes. - Serve -> DRF viewsets at
/api/projects/{id}/mcp_analytics/{sessions,intent_clusters,feedback,missing_capabilities}(router inbackend/presentation/urls.py) plus custom actions (sessions/{id}/tool_calls,sessions/{id}/generate_intent,sessions/intent_digest,sessions/activity_overview,intent_clusters/recompute). Parallel surface: step 4's runners, exposed to agents as thequery-mcp-*tools. The intent-cluster read and recompute endpoints requiremcp-analytics-intent-routing; the other endpoints usemcp-analytics. - Frontend -> Kea scene
MCPAnalyticsScene.tsx, with tabs enumerated byMCPAnalyticsTabinmcpAnalyticsSceneLogic.ts: activity, dashboard, sessions, tool quality, intent clustering, notifications. The landing tab is volume-gated bydashboardStageinmcpAnalyticsOnboardingLogic.tsand applies only to the bare/mcp-analyticsredirect — deep links and explicit tab clicks are never overridden. The intent clustering tab, dashboard KPI, and tool-detail cluster section are all gated bymcp-analytics-intent-routing; a direct unflagged link renders the standard not-found page.- Activity (
earlyData/): live tool-call feed plus the intent-themes card. "Theme" (the LLM digest, Activity tab) is not "cluster" (the embedding clustering, its own tab). Conflating the two is the most common mistake here. - Tool quality and the per-tool tool report (
MCPAnalyticsToolDetail.tsx, its own registered scene): shared date filter, failure-occurrence drill-down with copyable error context, and "create fix task" straight intoproducts/tasks. - Dashboard: quill composable
Metrictiles and@posthog/quill-primitives, plus notable sessions selected by aNotableRule— so that table can legitimately be short or empty. - Notifications: first-party destinations for MCP events and recurring AI reports
(
frontend/notifications/), thin wiring over the generic hog-function destination and subscription machinery.
- Activity (
Postgres models (backend/models.py): MCPSession (the intent store),
MCPIntentClusterSnapshot, MCPAnalyticsSubmission (feedback and missing-capability
reports), MCPIntentEmbeddingCache.
Seeding local data: ./manage.py seed_mcp_sessions --team-id N
(backend/management/commands/), with --sessions, --min-calls/--max-calls, --days,
--missing-capabilities, --seed, and --clear. Seeded events are tagged $mcp_seeded so
--clear removes only seeded data.
Which repo to change
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 40k
- Forks
- 3k
- Last commit
- Sep 2026
ahel review
K1binfo
installs-packages
Automated review, not a security audit. Ruleset v1+k2.
Advanced
- Catalog kind
- skill
- Gateway key
debugging-mcp-analytics- Source
- github.com/posthog/posthog