Skip to content

feat(mcp): opt-in local Guide telemetry [roadmap:v0.10.4] - #72

Merged
tcballard merged 6 commits into
mainfrom
claude/mcp-telemetry-opt-in-0nucfg
Jun 12, 2026
Merged

feat(mcp): opt-in local Guide telemetry [roadmap:v0.10.4]#72
tcballard merged 6 commits into
mainfrom
claude/mcp-telemetry-opt-in-0nucfg

Conversation

@tcballard

@tcballard tcballard commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements rac/roadmaps/v0.10.x-guide/v0.10.4-guide-telemetry.md.

Adds:

  • Opt-in, default-off, local-only usage telemetry for the Guide MCP server: rac mcp --telemetry appends one JSONL event per tool call (counts and metadata only — never arguments or repository content) to $XDG_STATE_HOME/rac/guide-telemetry.jsonl
  • rac mcp-stats [--json | --share]: local read-back summary; --json is the shareable export; --share prints a prefilled GitHub usage-report issue URL the user reviews and submits in their own browser
  • ADR-040 recording the telemetry shape, a 27-case telemetry battery, three mcp-stats goldens, a guide-usage-report issue form, and documentation in docs/mcp.md, docs/cli.md, and the README trust section

Roadmap / ADR Trace

Roadmap:

  • rac/roadmaps/v0.10.x-guide/v0.10.4-guide-telemetry.md

Relevant ADRs:

  • rac/decisions/adr-040-guide-local-telemetry.md (new — opt-in, local-only, content-free telemetry; numbered 040 because ADR-039 was claimed by the Lore server identity decision merged on main while this branch was in flight)
  • rac/decisions/adr-032-guide-stateless-reads.md (tool output stays a pure function of repository bytes + input)
  • rac/decisions/adr-035-byo-ai-credentials.md (no RAC cloud dependency)
  • rac/decisions/adr-030-guide-tools-only-surface.md (tool descriptions untouched)
  • rac/decisions/adr-034-guide-agent-reasoning-boundary.md (outcome classification reads the structured error tokens)

Scope

Included

  • Recorder module rac.mcp.telemetry (stdlib-only): pinned event schema, per-process random session id, single-generation 1 MB rotation, never-raise posture (a recorder that cannot write disables itself for the process)
  • build_server(..., recorder=None) additive parameter; each tool body lifted to a module-level function and routed through telemetry.observe, which returns the payload unchanged
  • run_server(..., telemetry_enabled=False) with a one-line stderr notice when on — opt-in is announced, never silent
  • rac mcp-stats human and JSON output; exit 0 for any produced summary including an empty or missing log
  • Share flow as string formatting only: the URL targets .github/ISSUE_TEMPLATE/guide-usage-report.yml and excludes the local log path (a home-directory path can embed a username); RAC contains no network code
  • Telemetry battery registered in the CI matrix; mcp-stats goldens over a committed fixture log with a relative XDG_STATE_HOME so golden output stays machine-independent

Excluded

  • Automatic upload, remote endpoints, or any background transmission (rejected in ADR-040)
  • Environment-variable or config-file enablement — persistent invisible state is worse for trust than a flag visible in the client's server config
  • Recording tool arguments, artifact IDs, queries, or content under any setting
  • Retention policy beyond one rotated generation; latency percentiles or query-shape analytics (a future schema decision, not a patch)
  • Telemetry for CLI commands or Explorer — this is a Guide surface only

Product / Architecture Decisions

  • Telemetry is write-only observability outside the request/response contract: observe returns the serialized payload byte-for-byte, and the log is never an input to a response — ADR-032's determinism survives intact and is pinned by tests
  • Tool bodies were lifted to private module-level functions (_get_artifact, _search_artifacts, _get_related, _get_summary) rather than wrapped by a generic decorator, so FastMCP's signature-derived input schemas and the verbatim ADR-030 descriptions are untouched
  • Summary logic lives in rac.mcp.telemetry, not rac.services — services may not import the server layer, and the module's stdlib-only imports satisfy the isolation battery by construction
  • The event's truncated field is always present (explicit booleans make counting trivial), unlike the wire payload where absence-means-false is the pinned contract; the log is a separate contract
  • Exception outcomes are recorded then re-raised — telemetry never swallows a failure the agent should see
  • mcp-stats is a new flat subcommand rather than an extension of rac stats, whose exit-code semantics are tied to corpus content
  • Log path is $XDG_STATE_HOME/rac/ (state, not config or data), matching the Explorer workspace precedent

User-Facing Contract

CLI

rac mcp --root /path/to/repo --telemetry
rac mcp-stats
rac mcp-stats --json
rac mcp-stats --share

Human Output

  • rac mcp --telemetry announces on stderr: rac mcp: telemetry on — appending tool-call events (no arguments, no content) to ... followed by the log path (stdout stays protocol-only)
  • rac mcp-stats prints the log path, event/session counts, first/last timestamps, a per-tool usage table (calls, errors, truncated, avg ms), and a skipped-unreadable-lines note when nonzero; with no log it prints guidance: telemetry is off by default, enable with rac mcp --telemetry

JSON Output

{
  "schema_version": "1",
  "path": "local log path (omitted from the --share payload)",
  "event_count": "total events",
  "session_count": "distinct per-process session ids",
  "first_ts": "ISO 8601 UTC or null",
  "last_ts": "ISO 8601 UTC or null",
  "skipped_lines": "unreadable lines tolerated during read-back",
  "tools": "per-tool rows: tool, calls, errors, truncated, avg_duration_ms"
}

Event lines in the log carry the pinned field set: schema_version, ts, session, tool, outcome (ok | error | exception), error (structured token, error outcomes only), duration_ms, truncated.

Exit Codes

  • 0: mcp server shutdown on client disconnect; mcp-stats summary produced, including from an empty or missing log
  • 2: usage error (--json with --share; mcp --root not a directory)

Verification

Ran

python -m pytest                      # 964 passed (re-run after rebasing onto the Lore rename)
ruff check .                          # clean
mypy                                  # no new errors (remaining are pre-existing missing-stub noise)
rac validate rac/                     # 106 valid, exit 0
rac relationships rac/ --validate     # 266 checked, 0 issues, exit 0
rac review rac/                       # no priority 1-2 findings

Manual end-to-end with a real stdio client against the dogfood corpus: drove get_summary, search_artifacts, and a not-found get_artifact with --telemetry — three events recorded with correct outcome classification (including a genuinely truncated get_summary and the not-found error token), no arguments or content in the log, rac mcp-stats and --json consistent. Re-ran the same client without the flag: zero new lines.

Covered

  • Default off: no recorder, no file touched; run_server without the flag builds no recorder
  • Pinned event schema field-for-field, including the error-outcome field set and timestamp format
  • Content-free guard: the artifact ID argument, the search query, and the returned file content are asserted absent from the log text
  • Payload stability: tool responses byte-identical with a recorder attached vs. none, across ok, error, and truncated shapes — existing Guide goldens pass unchanged, which is the proof the wire contract did not move
  • Write failure never breaks a call; recorder self-disables and stays silent
  • Rotation at the size cap; corrupted-log read-back skips and counts bad lines; missing file is an empty log
  • Share URL round-trips to the JSON summary minus the local path; --json/--share mutual exclusion exits 2

Review Path

  1. rac/decisions/adr-040-guide-local-telemetry.md and rac/roadmaps/v0.10.x-guide/v0.10.4-guide-telemetry.md — the contract
  2. src/rac/mcp/telemetry.py — recorder, schema, read-back, share URL
  3. src/rac/mcp/server.py — body lift and observe routing (diff is mostly indentation-neutral moves)
  4. src/rac/cli.py, src/rac/output/human.py, src/rac/output/json.py — flag, mcp-stats, renderers
  5. tests/test_mcp_telemetry.py, tests/test_golden.py, tests/fixtures/telemetry/ — the battery and goldens
  6. docs/mcp.md, docs/cli.md, README.md, .github/ISSUE_TEMPLATE/guide-usage-report.yml — user-facing surface

Notes For Reviewer

  • The branch is rebased onto the Lore server rename (chore(guide): rename the agent-facing server to lore [release:v0.10.4] #71); the telemetry decision was renumbered to ADR-040 in a dedicated commit because main claimed ADR-039 mid-flight.
  • The --share payload deliberately omits the path field from the --json shape: the report is counts and timestamps only, and a state-home path can embed a username. The share golden pins this.
  • GitHub issue-form query-parameter prefill (?template=...&report=...) is the one external behavior assumed; the roadmap records it as an assumption. If GitHub drops prefill, --share degrades to an unfilled form — the JSON export path is unaffected.
  • The opt-in posture means sparse, self-selected data; ADR-040 accepts this as the cost of the trust posture and sets a review date if no one opts in within a quarter.
  • tests/test_mcp_isolation.py required no changes — the stdlib-only telemetry module satisfies the existing consumer-boundary rules by construction.

Implementation Process

Implemented with AI assistance under the roadmap contract.

Final scope, review, and acceptance decisions were made by the maintainer.

Implements rac/roadmaps/v0.10.x-guide/v0.10.4-guide-telemetry.md (contract).

Adds:
- v0.10.4 roadmap: opt-in local Guide telemetry milestone
- ADR-039: opt-in, default-off, local-only, content-free telemetry;
  share flow is a user-submitted GitHub issue, no network code in RAC
Implements rac/roadmaps/v0.10.x-guide/v0.10.4-guide-telemetry.md.

Adds:
- rac.mcp.telemetry: stdlib-only recorder, pinned JSONL event schema,
  size rotation, corruption-tolerant read-back, summary, share URL
- build_server recorder parameter; run_server telemetry flag with
  stderr notice; tool payloads return byte-identical (ADR-032 holds)
…10.4]

Implements rac/roadmaps/v0.10.x-guide/v0.10.4-guide-telemetry.md.

Adds:
- rac mcp --telemetry: explicit opt-in, announced on stderr
- rac mcp-stats [--json | --share]: local summary, JSON export, and a
  prefilled usage-report issue URL (the user's browser transmits)
- guide-usage-report issue form for shared reports
…0.4]

Implements rac/roadmaps/v0.10.x-guide/v0.10.4-guide-telemetry.md.

Adds:
- telemetry battery: default-off, pinned event schema, content-free
  guard, byte-identical payloads with and without a recorder,
  write-failure tolerance, rotation, corrupted-log read-back
- mcp-stats goldens (human, JSON, share URL) over a fixture log
Existing Guide goldens are untouched — the wire contract did not move.
Implements rac/roadmaps/v0.10.x-guide/v0.10.4-guide-telemetry.md.

Adds:
- docs/mcp.md telemetry section: what is recorded, what never is,
  the log path, read-back, and the share flow
- docs/cli.md entries for mcp (--telemetry) and mcp-stats
- README trust bullet and changelog entry
…0.4]

ADR-039 was claimed by the Lore server identity decision merged on main
while this branch was in flight; the telemetry decision and every
reference to it move to ADR-040. No behavior change.
@tcballard
tcballard force-pushed the claude/mcp-telemetry-opt-in-0nucfg branch from 3ecb1c3 to 7622dd9 Compare June 12, 2026 17:20
@tcballard
tcballard merged commit 0cc53b1 into main Jun 12, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant