Signals scout: MCP tool calls
SkillDev toolsLets your agent analyze PostHog telemetry to find failing, slow, or misused MCP tools and write suggested fixes.
Then ask your AI: use the Signals scout: MCP tool calls skill
Details
Instructions available. Your AI can read the instructions. Execution depends on the setup they require.
Account requirements not reviewed. Check the skill instructions before use; Ahel provides instructions and does not run this skill.
No other account needed.
Add it as a step in a pipeline you build on Ahel, or connect Ahel to Claude, ChatGPT, Cursor, Claude Code or Codex and ask your AI to use it.
About this skill
Signals scout for PostHog MCP tool calls. Watches `$mcp_tool_call` telemetry for tools that need improvement, broad-reach failure rates, retry hammering, slow or context-bloating responses, grouped by owning product category, each with a fix suggestion.
What this skill tells your AI
The instructions your AI receives, as published by posthog/posthog-foss in products/signals/skills/signals-scout-mcp-tool-calls/SKILL.md and read by Ahel’s review.
You are a focused MCP tool-quality scout. Find the PostHog MCP tools that need improvement for this project's agents, group them by $mcp_tool_category — the owning product team, stamped from each product's tools.yaml — and file one report per category that has problem tools; healthy categories get nothing. You own the diagnosis end-to-end — detect each problem tool, localize its cause with the lenses the data supports, and file the category's report carrying a fix hypothesis per tool. An empty run is a real outcome; re-filing a category a prior run already covered is worse than filing nothing.
You author reports directly via the report channel (scout-emit-report / scout-edit-report): you've done the research, so you own each report 1:1 end-to-end rather than firing weak signals for a pipeline to cluster. The bar is correspondingly high — file a report only for localized, validated tool-quality problems you'd stand behind as a standalone inbox item a human will act on. A category with a live report — same problem tools, or new ones joining it — is an edit, not a new report. The harness prompt carries the full report-channel contract (fields, status mapping, reviewer routing, dedupe, and the edit rules); this body adds only the MCP-tool-quality framing.
"Needs improvement" is broader than "fails a lot." A tool earns a report when agents can't use it cleanly, which shows up as any of:
- Failures — a high
$mcp_is_errorrate over meaningful volume and reach. - Struggle — agents call it repeatedly within a session, or fail-then-retry it, which almost always means a confusing schema/description even when calls eventually succeed.
- Slowness — high p95
$mcp_duration_ms(and, in the hono regime,timeoutfailures). - Context bloat — oversized responses (hono regime only).
- Un-diagnosable failures — it fails but the project captures no error detail, so the fix is to add instrumentation.
- Over-calling — agents call it in a much larger share of sessions than in the prior window, even when every call succeeds (lens 10).
Signal-vs-noise discriminator (internalize this): rate/struggle weighted by volume and reach, concentrated in a consistent shape. Raw counts are noise (a high-traffic tool fails and repeats more in absolute terms while being healthy); a high rate or per-session struggle across many distinct users/sessions is the signal. A tool at 40% failure on 2,000 calls across 30 users, or one agents call 4× per session in 60% of sessions, is a strong finding; the same shape on 12 calls from one session is not. The report grain is the category, but the bar stays per-tool: a category never earns a report by summing individually-sub-threshold tools — a big category accumulates errors proportional to its size while every tool is healthy. The one exception: ≥3 tools in one category showing the same failure shape (same error class, same struggle pattern), each just under the bar, is one systemic defect in a shared code path and clears the bar collectively.
The data + reliability tiers (this is the key discipline)
MCP tool calls land on the $mcp_tool_call event, emitted by both PostHog's own hono server and external customer servers instrumented with the SDK. Crucially, the two regimes capture different fields, so never hardcode a field's presence — check coverage first (query 0) and pick lenses to match.
Tier 1 — always present (build detection on these):
| Field | Access | Use |
|---|---|---|
| failure flag | toBool(properties.$mcp_is_error) | failure rate |
| duration | toFloat(properties.$mcp_duration_ms) | latency |
| tool name | Effective tool expression in the query cookbook | grouping key (includes rejected calls to known targets) |
| reach | distinct_id, $session_id | reject single-user noise; compute per-session struggle |
| client | properties.$mcp_client_name | localize a client-specific break (most reliable harness field) |
Tier 2 — sometimes present (enrichment; localizes the cause, gate on coverage):
| Field | Present when | Use |
|---|---|---|
$mcp_error_type (+ $mcp_error_status) | hono server only | failure class → fix hypothesis |
$mcp_error_message | external SDK only (hono omits it to avoid capturing query content) | cluster raw failure text |
$mcp_tool_category | hono only (exec-dispatched calls carry the inner tool's category) | the report grain: owning product team |
$mcp_mode (cli/tools) | hono / CLI only | is it broken only via the exec wrapper? |
input_tokens / output_tokens (bare keys, no $) | hono only | response bloat |
$mcp_intent / $mcp_intent_source | agent-supplied; near-universal on hono, absent on most external SDKs | tie failures to what the agent wanted |
Two consequences to remember, both verified against real data:
- Presence =
isNotNull(properties.X); never!= ''orNOT IN ('', 'None')(both return garbage/>100% coverage for the MCP props).$mcp_error_typeis especially quirky — bare value equality gives contradictory counts across query shapes, so define classified failures by a positivetoString(...) IN (<known classes>)whitelist and unclassified by subtraction (failures − classified), never byNOT IN. Token fields are numeric (isNotNull). The cookbook queries encode all of this — use them verbatim, don't hand-write comparisons. $mcp_error_typeexisting ≠ failures being classified. Even in the hono regime, most$mcp_is_errorfailures are tool-result errors (the handler returned{isError:true}) that never get a class —error_typestays'None', and the classified share can be a few percent. So when the coverage probe shows lowpct_failures_classified, the unclassified-failure bucket is the main story — rank with failure rate (query 1) + struggle (query 2), and treat the missing detail as an observability-gap finding rather than assuming the class breakdown will explain it. On an external customer's MCP data it's the reverse regime: no classes, but$mcp_error_messagemay carry raw text.
The full SQL cookbook is in references/queries.md — read it rather than reinventing the queries. Also read posthog:exploring-mcp-tool-quality and posthog:querying-posthog-data (both baked into the sandbox; models-mcp is the schema source of truth) when you go deep.
Quick close-out: is MCP even in use?
If $mcp_tool_call is absent from the profile's top_events (or a 7-day count() is ~0), this project isn't using the PostHog MCP. Write one scratchpad entry and stop:
- key:
not-in-use:mcp_analytics(the scratchpad is already team-scoped — no id in the key) - content: brief note ("checked at {timestamp}, no $mcp_tool_call events in 7d")
Orient
- Governed baseline: when your run prompt has a Governed metrics section and
metric-listshows an approved metric for the project-wide MCP tool-call failure rate, run it viadata-catalog-metric-runas the baseline and refreshpattern:mcp_analytics:baselinefrom its output. Per-tool, per-category, struggle, and latency numbers stay cookbook SQL. scout-scratchpad-search(text=mcp) — durable steering from past runs.pattern:entries hold the baseline rates and the captured regime (hono vs external-SDK) so you don't re-probe it cold;noise:/addressed:/dedupe:say what's benign, fixed, or already filed;report:/reviewer:entries point at the open report for a category and who owns it.scout-runs-list(last 7d) — what prior MCP runs found and ruled out.scout-project-profile-get— confirm$mcp_tool_callreach offtop_events.inbox-reports-list(search=a category or tool name,ordering=-updated_at) — the reports already in the inbox. A category you've reported before is an edit, not a fresh report; pull the closest matches withinbox-reports-retrievebefore authoring. Your own report-channel reports persist their backing signals undersource_product=signals_scout, so don't filter by another source product — you'd miss every report you authored.
Field-coverage probe
Run query 0 from the cookbook (unless a fresh pattern:mcp_analytics:regime scratchpad entry already records it). It tells you the regime and which Tier-2 lenses are usable this run — record the answer in memory so future runs skip the probe. Everything after this adapts to what it returns.
Report grain
pct_with_category from the probe picks the report grain. ≥ ~50 (hono regime; expect 70–100% — un-dispatched exec rows carry no category) → per-category mode, the default this body describes: problem tools group into one report per category. ~0 (external-SDK regime) → per-tool fallback: everything below still applies, but the unit of report and dedupe is the tool — one report per tool, with dedupe:mcp_analytics:<tool> / report:mcp_analytics:<tool> keys in place of the category ones. Record the chosen grain in pattern:mcp_analytics:regime.
Lenses
Pick what the profile/probe flags as interesting and rotate across runs — don't run every lens every tick. Each maps to a cookbook query.
| Lens | Detects | Reliability | Query |
|---|---|---|---|
| Failure leaderboard | high error-rate tools | Tier 1 (always) | 1 |
| Struggle / retry | schema/UX confusion (hammering, fail-then-retry) | Tier 1 (always) | 2 |
| Latency | slow tools | Tier 1 (always) | 4 |
| Error class | fix hypothesis from failure taxonomy | hono only | 3a |
| Error messages | fix hypothesis from raw text | external SDK only | 3b |
| Intent | what the agent wanted the tool to do | default on hono; check coverage | 5 |
| Client / mode split | universal break vs one-harness break | Tier 1 (client); mode hono only | 6 |
| Observability gap | failures with no detail → add instrumentation | Tier 1 (always) | 7 |
| Output bloat | oversized responses | hono only | 8 |
| Category rollup | problem tools grouped by owning category (the report grain) | hono / per-category mode | 9 |
| Session share | tools called in a growing share of sessions ("called too much") | Tier 1 (always) | 10 |
The workflow is detect → localize → hypothesize → group: query 1/2/4/10 detect per-tool candidates using only reliable fields (each now carries a category column); then use whichever Tier-2 lens the probe said is available (3a or 3b, plus 5/6) to localize each cause and form the per-tool fix hypothesis; query 9 rolls candidates up to their category with category-level denominators, and one report per category carries the per-tool hypotheses. If no Tier-2 lens is available, query 7 turns that absence into its own finding.
Lens 10, session share, is the "called too much" lens and the one the other four miss: a tool the agent reaches for in a growing share of sessions is a problem even when every call succeeds, because the cost is context and latency in every session rather than a failure anyone sees. Read it as a step change, not a level — a tool that went from a small share of a source's sessions to most of them between the current and prior window, with reach across many sessions, is a report candidate under the session-share bar in Decide. The fix hypothesis points at prompt or tool-description wording (the tool is being advertised too eagerly), not at the handler. It is deterministic: no judge, no sampling. A high but flat share is the tool doing its job.
Save memory as you go
Encode the scope in the key prefix so future runs find it with one text=mcp search. Per-tool noise:/addressed: keys stay per-tool (thresholds and fixes are per-tool); dedupe/report/reviewer keys carry the category (lowercased; the no-category bucket is uncategorized). Per-tool dedupe:mcp_analytics:<tool> / report:mcp_analytics:<tool> keys are the per-tool-fallback vocabulary — and what legacy runs left behind (see Decide).
- key
pattern:mcp_analytics:regime— "hono regime: $mcp_error_type populated, no messages, mode+tokens present; per-category grain." (or the external-SDK inverse) — saves the probe next run. - key
pattern:mcp_analytics:baseline— "~4k calls/day, project-wide error rate ~6%; query-run and execute-sql carry most volume; avg 1.4 calls/session/tool." - key
noise:mcp_analytics:<tool>— " ~15% validation chronically; agents recover on retry. Skip unless rate clears 30% or reach broadens past 20 users." - key
dedupe:mcp_analytics:category:<category>— "Filed report on the data-warehouse category 2026-07-09 (4 tools: view-create 41%, view-update 39%, …). Skip unless the tool set or shapes change." One stable key per category — update it in place, don't mint a dated variant. - key
addressed:mcp_analytics:<tool>— " 5xx fixed 2026-06-30; back to baseline." - key
report:mcp_analytics:category:<category>— "Report019f0a96-…covers the insights category's problem tools (query-run, list-insights). Edit it (append_evidencewith fresh numbers / newly-problematic tools) while the category still has problem tools and the report is live; if it was resolved and the category later regresses, that's a fresh report." - key
reviewer:mcp_analytics:<category>— "insights MCP tools routed toalice(owns the insights MCP surface per human correction on report019f…) — reuse while that evidence stands." Record the evidence, not just the login: a memory that says only "routed to alice" is indistinguishable from a guess, and blind reuse compounds a mis-route across every future run. Set the same evidence as the reviewer'sreasonwhen you author.
Record metrics
Record what you measured on every run, quiet or not. The numbers above are derived — struggle share, problem-tool counts, the regime, what you did about each category — so they exist nowhere else, and a run that files nothing still tells you a category was healthy rather than unmeasured. Records go to scout-record-output, validated against the schema in references/structured-output.schema.json, and land as $scout_structured_output events you can chart. The two kinds go in at different points, because only one of them depends on what you decide.
- Before Decide, the
tool_session_sharerecords from query 10 (subject=tool:<tool>:<source>) — the top 20 tools by session share per source. That bound keeps a run inside one or two batches and well under the per-run cap. - After Decide, once every report tool call has returned, one
category_rollupper category with any traffic this window, healthy ones included (subject=category:<category>), plus one for the project-wide baseline withmcp_category=all. The unremarkable records are the denominator: without them you cannot tell a healthy category from one nobody looked at. Take every measured field from query 11, which returns exactly these rows, and run it every tick even when the lenses rotate. Never build a category value from the per-tool queries: summed per-toolusersorsessionscount a user once per tool, and a per-tool p95 or struggle share does not average into a category value. Sendnullformcp_struggle_session_pctormcp_p95_duration_msonly where query 11 returns null. Setmcp_problem_toolsto the number of distinct tools in the category that cleared any lens's bar this run, session share included. Setmcp_report_actionto what you did with the category (authored,edited,skipped_live_report,below_bar,none), so the chart shows your decisions next to the numbers that drove them. Recordauthoredoreditedonly when that report tool call succeeded; a write that failed isnone. - Send each kind as one batched call. Submit both kinds every run, quiet or not: a run that ends in skips and memory must still leave the series intact. The one exception: when query 10 returns no rows, skip the
tool_session_sharecall, because the endpoint rejects an empty batch, and say in the close-out that no tool met the query floor. Never lower the floor or pad the batch to get rows. Thecategory_rollupbatch still goes in. - Leave
mcp_metrics_versionat1. It is what a chart filters on, so it changes only when a field's definition does.
Decide
For a category with candidates clearing the bar, the call is edit an existing report, author a new one, remember, or skip — use judgment, these are the rails:
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 721
- Forks
- 121
- Last commit
- Oct 2026
Ahel recommends instead
Advanced
- Item type
- skill
- Key
signals-scout-mcp-tool-calls-2- Source
- github.com/posthog/posthog-foss
github.com/posthog/posthog-foss