Signals scout: MCP tool calls

SkillDev tools

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.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Signals scout: MCP tool calls skill

What this skill tells your AI

The instructions your AI receives, as published by posthog/skills in skills/omnibus/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:

  1. Failures — a high $mcp_is_error rate over meaningful volume and reach.
  2. 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.
  3. Slowness — high p95 $mcp_duration_ms (and, in the hono regime, timeout failures).
  4. Context bloat — oversized responses (hono regime only).
  5. Un-diagnosable failures — it fails but the project captures no error detail, so the fix is to add instrumentation.

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):

FieldAccessUse
failure flagtoBool(properties.$mcp_is_error)failure rate
durationtoFloat(properties.$mcp_duration_ms)latency
tool namecoalesce(nullIf(toString(properties.$mcp_exec_tool_call_name), ''), toString(properties.$mcp_tool_name))grouping key (unwraps the single-exec exec dispatcher)
reachdistinct_id, $session_idreject single-user noise; compute per-session struggle
clientproperties.$mcp_client_namelocalize a client-specific break (most reliable harness field)

Tier 2 — sometimes present (enrichment; localizes the cause, gate on coverage):

FieldPresent whenUse
$mcp_error_type (+ $mcp_error_status)hono server onlyfailure class → fix hypothesis
$mcp_error_messageexternal SDK only (hono omits it to avoid capturing query content)cluster raw failure text
$mcp_tool_categoryhono only (exec-dispatched calls carry the inner tool's category)the report grain: owning product team
$mcp_mode (cli/tools)hono / CLI onlyis it broken only via the exec wrapper?
input_tokens / output_tokens (bare keys, no $)hono onlyresponse bloat
$mcp_intent / $mcp_intent_sourcesparse, opt-in (agent-supplied)tie failures to what the agent wanted

Two consequences to remember, both verified against real data:

  • Presence = isNotNull(properties.X); never != '' or NOT IN ('', 'None') (both return garbage/>100% coverage for the MCP props). $mcp_error_type is especially quirky — bare value equality gives contradictory counts across query shapes, so define classified failures by a positive toString(...) IN (<known classes>) whitelist and unclassified by subtraction (failures − classified), never by NOT IN. Token fields are numeric (isNotNull). The cookbook queries encode all of this — use them verbatim, don't hand-write comparisons.
  • $mcp_error_type existing ≠ failures being classified. Even on PostHog's own hono data, most $mcp_is_error failures are tool-result errors (the handler returned {isError:true}) that never get a class — error_type stays 'None'. On PostHog's own project only ~4% of failures carry a real class. So when the coverage probe shows low pct_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_message may 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 first: when your run prompt's catalog listing shows an approved mcp_tool_call_fail_pct metric, run it via data-catalog-metric-run as the canonical project-wide failure-rate baseline and refresh pattern:mcp_analytics:baseline from its output. Per-tool, per-category, struggle, and latency numbers stay cookbook SQL — label them noncanonical whenever the governed baseline exists.
  • 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_call reach off top_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 with inbox-reports-retrieve before authoring. Your own report-channel reports persist their backing signals under source_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.

LensDetectsReliabilityQuery
Failure leaderboardhigh error-rate toolsTier 1 (always)1
Struggle / retryschema/UX confusion (hammering, fail-then-retry)Tier 1 (always)2
Latencyslow toolsTier 1 (always)4
Error classfix hypothesis from failure taxonomyhono only3a
Error messagesfix hypothesis from raw textexternal SDK only3b
Intentwhat the agent wanted the tool to doif pct_with_intent ≥ ~205
Client / mode splituniversal break vs one-harness breakTier 1 (client); mode hono only6
Observability gapfailures with no detail → add instrumentationTier 1 (always)7
Output bloatoversized responseshono only8
Category rollupproblem tools grouped by owning category (the report grain)hono / per-category mode9

The workflow is detect → localize → hypothesize → group: query 1/2/4 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.

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>"Report 019f0a96-… covers the insights category's problem tools (query-run, list-insights). Edit it (append_evidence with 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 to alice (owns the insights MCP surface per human correction on report 019f…) — 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's reason when you author.

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:

  • Search the inbox first. The report:mcp_analytics:category:<category> scratchpad pointer is the reliable path (it holds the report_idinbox-reports-retrieve it directly); with no pointer, inbox-reports-list by the category name and by each problem tool's name (ordering=-updated_at) — the tool-name search is what catches legacy per-tool reports. A category with a live report and no material change is a skip.
  • Edit (scout-edit-report) when a still-live report already covers the category and it still has problem tools — the same ones, or new ones joining. Add the fresh numbers (per-tool rate trends, broadening reach) and any newly-problematic tools with append_evidence, each with its own hypothesis; or rewrite the title/summary on a report you authored. This is the default when a match exists. edit-report can't change status, so if the matched report is resolved / suppressed / failed, don't append (it won't resurface) — author a fresh report for the relapse and repoint the report: key.
  • Author (scout-emit-report) only when nothing live covers the category — one report per category (tools aggregated over the window), never one per tool or per failed call. A report-worthy finding: confidence ≥ 0.85, each listed tool's problem (failure / struggle / latency / bloat) high over the volume floor with reach across multiple users/sessions, and — when a Tier-2 lens is available — localized to a class/message/intent with counts in the evidence. Below that bar, write memory instead. The title names the category and the scale ("MCP data-warehouse tools need improvement: 4 tools failing/struggling"). The summary follows Hook (category + N problem tools + combined volume/reach + the category's share of project MCP traffic from query 9) → one short block per problem tool (the quantified problem, its shape from the Tier-2 lens, the fix hypothesis) → Recommendation. Write for an engineer on the owning team who's never seen these tools, and state which regime the evidence came from. Attach the category's shape via charts — a per-tool bar of the active lens's metric (failure rate, struggling-session share, p95 duration, or response bloat), plus a daily series of that same metric when a regression is dated — so the reader sees the problem tools at a glance. Set priority (P0–P4) + priority_explanation — what the worst tool would earn alone (P2 if any tool is high-rate/high-struggle, broad-reach, clearly-localized; P3 otherwise); bundling never raises priority. Set suggested_reviewers via scout-members-list (objects — a {github_login} or {user_uuid}, not bare strings; cache under reviewer:mcp_analytics:<category>, and check prior category reports' artefacts via inbox-report-artefacts-list for precedent); left empty the report reaches no one. Then choose the actionability + repo together:
    • In the hono regime the tools live in PostHog's own MCP server, so unless this project's team owns that code, the action is an investigation / upstream report → actionability=requires_human_input and repository=NO_REPO (NO_REPO is what stops priority+reviewers from spawning a pointless repo-selection sandbox).
    • When the team owns the MCP server (the external-SDK regime, or PostHog's own project — the hono tools live in PostHog/posthog, handlers under services/mcp/ and each product's mcp/tools.yaml, so repository="PostHog/posthog") and the hypotheses are concrete code changes — a schema/description fix, a handler bug — → actionability=immediately_actionable with repository="owner/repo" (or omit repository to let the selector pick) to open a draft PR, and end the summary with the Fix loop metric section (next heading) so the implementation task iterates until the number moves. A category report bundling heterogeneous fixes across several tools leans requires_human_input even on an owned server — there's no single-PR shape; a single-tool or same-shape-systemic report can stay immediately_actionable.
    • After authoring, write the report:mcp_analytics:category:<category> pointer with the report_id so the next run edits instead of duplicating, and update the dedupe: entry.
  • Fold observability gaps in (query 7): a tool that fails materially (≥50 errors) with ≥90% of failures unclassified ($mcp_error_type IN ('', 'None') and no message) enters its category's report as an entry whose suggestion is "add error-type/message instrumentation to its MCP handler". Only a project-wide gap — failures undiagnosable across every category — is its own standalone P3 report; on a team-owned server the instrumentation change is concrete enough for immediately_actionable + the server repo, otherwise requires_human_input + NO_REPO.
  • Migrating legacy per-tool state: a live per-tool report (or a report:mcp_analytics:<tool> key) for a tool in problem category X — if that tool is X's only problem tool, edit that report in place (it is the category report de facto) and write report:mcp_analytics:category:<X> pointing at its id; if X has other problem tools too, author the category report noting it supersedes the legacy one, append_note the legacy report pointing forward, and repoint the keys. Never end a run with two live reports independently claiming the same tool.
  • Remember if below the bar or to record what you ruled out; skip with a one-line note if a noise: / addressed: / dedupe: entry or a live inbox report already covers it.

Fix loop metric — the autoresearch handoff

Every report filed immediately_actionable with a repository must end its summary with a Fix loop metric section. Its presence is what turns the auto-started implementation task from a one-shot patch into a measure → fix → re-measure loop, so it must stand alone for an agent that has never seen this scout or its cookbook:

  • Metric — one number matching the finding's primary lens, scoped to the category's problem tools over a trailing 7d window: failure rate for a failure finding, share of sessions with struggle (hammering / fail-then-retry) for a struggle/discoverability finding, p95 $mcp_duration_ms for latency.
  • Measurement — the exact HogQL to re-run (the cookbook query you used, inlined with the category/tool filters baked in — never a reference to "query 1"), plus the fastest local proxy: reproducing the cited failing calls against a dev MCP server, or the affected handler's unit tests.
  • Baseline — the value measured this run, with volume and reach ("41% of 2,013 calls across 34 users, 7d").
  • Goal — the direction and a concrete bar, e.g. decrease to at or below the project-wide error rate from pattern:mcp_analytics:baseline.
  • Constraints — the metric must move because the calls genuinely succeed: no masking errors, retry-hiding, swallowing exceptions, loosening schema validation to accept garbage, and no touching the $mcp_tool_call analytics stamping that feeds the measurement.
  • Evidence hygiene — the PR's before/after evidence shows aggregate metric outputs and synthetic/local reproductions only: raw telemetry rows, error messages, intent strings, and user/customer identifiers never appear in a PR or its attached images (the target repo may be public, and PR image assets stay readable forever).
  • Intent evidence — when pct_with_intent clears the bar, one line on what agents were trying to do when the tool failed (top $mcp_intent values), so the fix validates against real usage rather than the schema alone.

The cadence math is deliberate: the daily schedule + one report per category + autostart's one-implementation-task-per-report gate means each owning team receives at most one autonomous fix PR per day, aimed at its category's worst measurable problem. A category whose live report already has an implementation task in flight gets an edit with fresh numbers, never a second actionable report.

Disqualifiers (skip these)

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
62
Forks
6
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
signals-scout-mcp-tool-calls
Source
github.com/posthog/skills