Skip to content

feat(mcp): record local stdio tool calls at the dispatch chokepoint - #6432

Merged
loopover-orb[bot] merged 1 commit into
JSONbored:mainfrom
luciferlive112116:feat/mcp-local-telemetry-chokepoint
Jul 16, 2026
Merged

feat(mcp): record local stdio tool calls at the dispatch chokepoint#6432
loopover-orb[bot] merged 1 commit into
JSONbored:mainfrom
luciferlive112116:feat/mcp-local-telemetry-chokepoint

Conversation

@luciferlive112116

Copy link
Copy Markdown
Contributor

Summary

The local telemetry wrapper landed with the opt-in flag and the enable/disable/status commands, but nothing ever called it — a user could opt in and still have zero usage recorded. This wires it into the stdio tool-dispatch path.

registerStdioTool is already the single point every registered tool passes through, so no new chokepoint was needed. It now wraps each handler to time the call and record exactly once, on both the return and the throw path. Fields match the remote side — tool name, callerType: "local", ok, coarse duration — and nothing else. No tool's response changes.

Design notes

  • ok mirrors the remote's caller-visible outcome (response.status < 400) rather than merely "did it throw": a handler that reports failure by returning an error result is not a success. No handler does that today — they signal failure by throwing and the SDK maps it — so this only matters if one ever starts.
  • The opt-in read lives at module scope, deliberately. registerStdioTool(name, config, handler)'s second parameter is the tool's config and shadows the CLI's own module-level config. Reading the flag inside that function would have silently resolved the wrong object and never fired — a bug that would have looked like working code and produced exactly zero events.
  • Telemetry failures stay invisible to callers: the chokepoint keeps a defensive try/catch over recordMcpToolCall's own never-throw guarantee, mirroring recordMcpToolTelemetry on the remote.

Tests: verified on the wire, not mocked

The issue asks that default-off be verified, not just documented, so these tests don't mock the PostHog SDK. The stdio server runs as a real subprocess (where an in-process vi.mock couldn't reach it anyway), pointed at a local stand-in ingestion endpoint via LOOPOVER_MCP_POSTHOG_HOST. They assert what actually leaves the process:

Test Asserts
Default off Nothing is sent even with an API key present — and the tool still works
Opted in Exactly one mcp_tool_call event, with the allowlisted fields
Per invocation Two calls ⇒ two events (not one per session)
telemetry disable Returns the server to silence
Failure path A failing tool records ok=false and still fails identically for the caller

Opt-in is performed by running the real telemetry enable command, not by hand-writing the config file.

Two findings worth recording, both caught by the tests:

  1. The default-off tests were initially passing for the wrong reason. My recorder parsed the body as UTF-8, but posthog-node POSTs gzipped JSON to /batch/ — so parsing silently yielded nothing and every "sent nothing" assertion would have passed no matter what the CLI did. The recorder now gunzips. To prove the gate tests are not vacuous, I mutation-tested them: forcing telemetryEnabled: true makes both fail loudly (expected [ { event: 'mcp_tool_call' … } ] to deeply equal []). The three positive tests fail against the unwired CLI.
  2. The event carries 8 properties on the wire, not 4. Ours are exactly the allowlist (tool, caller_type, ok, duration_ms); the other four are the SDK's own $-prefixed vendor metadata — $lib, $lib_version, $is_server, $geoip_disable — which carry nothing about the user or the call. The test asserts the two sets separately, so a future field of ours can never hide among the vendor's. It also pins distinct_id === "loopover-mcp" (anonymous by construction), $geoip_disable === true, and that neither the commit message nor the PR body appears anywhere in the event.

Validation

  • New suite — 5/5 pass; the 3 positive tests fail against the unwired CLI, and the 2 gate tests fail under mutation.
  • 7 tool/telemetry suites — 77/77 pass (the wrapper touches every registered tool, so the blast radius is the whole tool surface).
  • Wide sweep of 14 further mcp suites — 12 clean; the only 2 failures (--body-file non-regular inputs) are pre-existing on clean main with my commit absent (Windows symlink restriction; they pass on CI's Linux).
  • npm run typecheck — 0 errors. npm run build:mcp — passes. git diff --check — clean. Rebased on latest main, no base conflict.

Coverage

No patch surface: coverage is collected over src/**, packages/loopover-engine/src/**, and packages/loopover-miner/lib/**. This PR touches only packages/loopover-mcp/** (not collected) and test/** (ignored). The behaviour is still covered end-to-end by the suite above.

Scope

  • One coherent change; wanted paths (packages/, test/). The remote server and the wrapper module are untouched — this is only the wiring the wrapper was written for.
  • No secrets: the API-key env var name is already public in the CLI's own help and lib/telemetry.js; the tests use a dummy key against a local endpoint.
  • No changelog, site/, CNAME, or lovable changes.

Safety

  • Zero data is sent unless the user has explicitly run loopover-mcp telemetry enable and an API key is configured — verified on the wire, not assumed.
  • No wallet/hotkey/reward/trust-score data, no tool arguments, and no source content can reach the event: the allowlist is the event shape, and the test pins it.
  • Cannot affect a tool response: telemetry is recorded after the handler resolves or throws, and its own failure is swallowed at two layers.

Closes #6238

The local telemetry wrapper landed with the opt-in flag and the
enable/disable/status commands, but nothing ever called it: a user could
opt in and still have zero usage recorded. Wire it into the stdio
tool-dispatch path.

registerStdioTool is already the single point every one of the registered
tools passes through, so no new chokepoint was needed -- it now wraps each
handler to time the call and record exactly once, on both the return and
the throw path. Fields match the remote side: tool name, callerType
"local", ok, and coarse duration. Nothing else, and no behaviour change to
any tool's response.

`ok` mirrors the remote's caller-visible outcome (`response.status < 400`)
rather than just "did it throw": a handler returning an error result is not
a success. No handler does that today -- they signal failure by throwing,
and the SDK maps it -- so this only matters if one ever starts.

The opt-in read lives at module scope, not inside registerStdioTool: that
function's second parameter is the TOOL's config and shadows the CLI's own
`config`, so reading the flag inside it would have silently seen the wrong
object and never fired.

Telemetry failures stay invisible to callers: the chokepoint keeps a
defensive try/catch over recordMcpToolCall's own never-throw guarantee,
mirroring recordMcpToolTelemetry on the remote.

Tests run the real stdio server as a subprocess against a local stand-in
PostHog endpoint rather than mocking the SDK, because the opt-in guarantee
is worth verifying on the wire and an in-process mock cannot reach a
subprocess anyway. They cover: nothing sent by default even with an API key
present, exactly one allowlisted event per call once opted in, one event
per invocation rather than per session, disable returning to silence, and a
failing tool recorded as ok=false while still failing identically for the
caller.

Closes JSONbored#6238
@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@codecov

codecov Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.61%. Comparing base (a2d8306) to head (dedcde4).
⚠️ Report is 17 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #6432   +/-   ##
=======================================
  Coverage   95.61%   95.61%           
=======================================
  Files         600      600           
  Lines       47346    47354    +8     
  Branches    15067    15068    +1     
=======================================
+ Hits        45268    45276    +8     
  Misses       1291     1291           
  Partials      787      787           
Flag Coverage Δ
shard-1 44.13% <ø> (+0.10%) ⬆️
shard-2 36.53% <ø> (-0.24%) ⬇️
shard-3 32.39% <ø> (+0.03%) ⬆️
shard-4 34.60% <ø> (+0.07%) ⬆️
shard-5 30.80% <ø> (-0.76%) ⬇️
shard-6 45.30% <ø> (+0.31%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.
see 1 file with indirect coverage changes

@loopover-orb loopover-orb Bot added the gittensor:feature Gittensor-scored feature linked to a feature issue — scores a 0.25x multiplier. label Jul 16, 2026
@loopover-orb

loopover-orb Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Tip

✅ LoopOver review result - approve/merge recommended

Review updated: 2026-07-16 07:52:30 UTC

2 files · 1 AI reviewer · no blockers · readiness 100/100 · CI green · clean

✅ Suggested Action - Approve/Merge

  • safe to merge

Review summary
This PR wires the previously-inert local telemetry wrapper into `registerStdioTool`, the single chokepoint every stdio tool registration passes through, recording exactly once on both return and throw paths. The `ok` computation mirrors the remote's `response.status < 400` semantics via `result?.isError !== true`, and the module-scope config read correctly avoids the parameter-shadowing bug the PR description calls out. Tests run the real subprocess against a local stand-in PostHog endpoint rather than mocking the SDK, directly verifying the default-off, opt-in, per-invocation-count, and ok=false-on-throw behaviors described.

Nits — 4 non-blocking
  • The flagged long-file size (packages/loopover-mcp/bin/loopover-mcp.js at ~940 lines) predates this PR's small addition and isn't something this diff should be expected to fix, but it's worth a follow-up issue if the file keeps growing.
  • recordStdioToolTelemetry (loopover-mcp.js) computes durationMs with Date.now() twice around an awaited handler call; consider using performance.now() for slightly more precise sub-ms timing, though Date.now() is fine given durations are reported coarsely.
  • Consider extracting the `result?.isError !== true` outcome check into a small named helper or comment reiterating the remote-mirroring rationale inline at the call site, since the design-notes explanation lives only in the PR description and the header comment — this is already done well via the header comment, so this is optional.
  • No changes needed to lib/telemetry.js in this diff, but it may be worth confirming (in a follow-up) that PostHog client instances created per-call in recordMcpToolCall are not leaking connections under high call volume — out of scope here since durations are coarse and calls are infrequent.

Decision drivers

  • ✅ Code review — No blockers (1 reviewer)
  • ✅ Gate result — Passing (No configured blocker found.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #6238
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ✅ 20/20 Low review scope from cached public metadata (1 linked issue).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 143 registered-repo PR(s), 81 merged, 32 issue(s).
Contributor context ✅ Confirmed Gittensor contributor luciferlive112116; Gittensor profile; 143 PR(s), 32 issue(s).
Improvement ✅ Minor risk: clean · value: minor · LLM: significant
Linked issue satisfaction

Addressed
The PR wraps registerStdioTool—the single chokepoint every local tool passes through—to call the telemetry wrapper with tool name, callerType 'local', ok, and duration, gated on the opt-in flag read at module scope, with defensive try/catch so telemetry failures never surface to the caller. Tests run the real stdio subprocess against a stand-in ingestion endpoint and verify zero events when opted

Review context
  • Author: luciferlive112116
  • Role context: outside_contributor
  • Public audience mode: oss maintainer
  • Lane context: Repository is configured for direct PR review.
  • Public profile languages: Python, JavaScript, MDX, Rust, TypeScript
  • Official Gittensor activity: 143 PR(s), 32 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Keep the PR focused and include validation evidence before maintainer review.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.
🧪 Chat with LoopOver

Ask LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.

  • @loopover ask &lt;question&gt; answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat &lt;question&gt; answers in natural prose from cached decision-pack facts via local inference (maintainer/collaborator; read-only).
  • A plain-language @loopover mention with a real question is routed to the closest matching read-only command automatically — no exact syntax required.

Full command reference: https://loopover.ai/docs/loopover-commands

🧪 Experimental — new and may change.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.

  • Re-run LoopOver review

@loopover-orb loopover-orb Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LoopOver approves — the gate is satisfied and CI is green.

@loopover-orb
loopover-orb Bot merged commit 45fe399 into JSONbored:main Jul 16, 2026
16 checks passed
@github-actions github-actions Bot mentioned this pull request Jul 16, 2026
12 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:feature Gittensor-scored feature linked to a feature issue — scores a 0.25x multiplier.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(mcp): instrument the local MCP tool-dispatch chokepoint with PostHog events (opt-in)

1 participant