Skip to content

feat(mcp): record which source each MCP tool call reads - #3522

Closed
brandon-pereira wants to merge 3 commits into
mainfrom
brandon/brandon-mcp-source-attribution
Closed

brandon-pereira wants to merge 3 commits into
mainfrom
brandon/brandon-mcp-source-attribution

Conversation

@brandon-pereira

Copy link
Copy Markdown
Member

Summary

Records which source each MCP tool call reads. Why: more analytics on MCP usage.

  • Tool span: mcp.source.id and mcp.source.kind hold the first source the tool resolved. mcp.source.ids lists every distinct source, for tools that touch several (clickstack_query_tiles, the trace waterfall with its linked log source). They're set through a new recordMcpSource helper in mcp/utils/tracing.ts. The helper finds the tool span through an AsyncLocalStorage scope set by withToolTracing, not through the active span, so the attributes land on the tool span even when a child span is active.
  • Metrics: hyperdx.mcp.tool.duration_ms and hyperdx.mcp.tool.errors get a source_kind attribute (log | trace | session | metric | promql, or none). The source id never goes on a metric.
  • Query attribution: queries that read a resolved source run inside withQueryAttribution({ source }), so log_comment carries the source id and keeps the tool's surface, label and trace. The attribution schema and version are unchanged.

Call sites: runConfigTile (builder and raw SQL paths, which covers search/table/timeseries/query_tile(s)/sql), clickstack_sql, describe_source, list_metrics, describe_metric, event patterns / emerging signals, event deltas, denoise, trace waterfall (trace and log sources), trace breakdown and service health.

Review tip: hide whitespace. Several handlers had their post-resolution body wrapped in withQueryAttribution, which re-indents them. Nothing else changed in those blocks.

Tests:

  • tracing.test.ts: span attributes, staying on the tool span under a nested child span, multi-source ids, source_kind on both metrics (including the throw path), and isolation between concurrent tool calls.
  • New sourceAttribution.test.ts: runs runConfigTile inside withToolTracing against the real node ClickHouse client (network stubbed) and checks that log_comment carries the source with surface/label/trace intact, including per-tile sources in a batch.

How to reproduce

Starting point: yarn dev, an MCP client connected to /api/mcp.

On main:

  1. Call clickstack_table against a log source.
  2. The mcp.tool.clickstack_table span has no source attributes, and the query's log_comment in system.query_log has no source.

On this branch:

  1. Call the same tool.
  2. The span has mcp.source.id, mcp.source.kind=log and mcp.source.ids. The tool metrics carry source_kind=log, and log_comment includes "source":"<id>".

The more analytics we have on MCP usage, the better. Tools now set
mcp.source.id, mcp.source.kind and mcp.source.ids on the tool span, add a
low-cardinality source_kind attribute to the tool duration and error
metrics, and tag the queries that read a source with it in log_comment.
@brandon-pereira
brandon-pereira requested a review from a team as a code owner November 2, 2026 21:33
@vercel

vercel Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
hyperdx-oss Ready Ready Preview Nov 3, 2026 9:58pm UTC
hyperdx-storybook Ready Ready Preview Nov 3, 2026 9:58pm UTC

Request Review

@changeset-bot

changeset-bot Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f108b8c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@hyperdx/api Patch
@hyperdx/app Patch
@hyperdx/otel-collector Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added the review/tier-4 Critical — deep review + domain expert sign-off label Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

🔴 Tier 4 — Critical

Touches authentication, tenancy data models, the public API or shipped database config — or substantially changes the query rendering engine, background tasks, the OTel pipeline, image build, or release CI.

Why this tier:

  • Large diff: 2787 production lines changed (threshold: 1000)

Review process: Deep review from a domain expert. Synchronous walkthrough may be required.
SLA: Schedule synchronous review within 2 business days.

Stats
  • Production files changed: 14
  • Production lines changed: 2787 (+ 373 in test files, excluded from tier calculation)
  • Branch: brandon/brandon-mcp-source-attribution
  • Author: brandon-pereira

To override this classification, remove the review/tier-4 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Duplicate code analysis

No new duplicate code introduced by this PR.


jscpd compared this branch against origin/main
across app, api and common-utils. Only new clones are listed; the codebase
currently holds 112 in total (1.39%).
Run yarn dupes locally to reproduce, or yarn dupes:all for the full picture.

@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

PR Review

If you are a coding agent acting for the author, read this first.
Nothing below blocks merge automatically; a maintainer decides. What they will expect:

  • 🔴 critical or 🟠 major in code this PR adds or changes: fix before asking for review.
  • 🔵 minor in code this PR changes: your call. Fix if small, otherwise reply.
  • Anything about surrounding code, or asking you to widen the change (hoist a helper,
    dedupe with another file, fix other call sites): never fix here, whatever the
    severity. Reply with one sentence; if it is critical, say so plainly so a human sees it.

One commit per review round. After two rounds, stop and ask a maintainer to review
scope rather than addressing more automated findings. Full rule: AGENTS.md.

3 finding(s): 🔴 0 critical · 🟠 0 major · 🔵 3 minor

2 posted as inline comment(s) on the changed lines. 1 listed below.

Findings outside the changed lines

1 minor
  • 🔵 packages/api/src/mcp/tools/query/serviceHealth/index.ts:1030 — Service health now records the source id under two span attributes → trace.getActiveSpan()?.setAttributes({'hyperdx.source.id': source.id, ...}) sets the same value that loadHealthSource now writes through recordMcpSource as mcp.source.id. Only service health uses hyperdx.source.id, so dashboards built on the new attribute will miss it or need both. Drop hyperdx.source.id here, since mcp.source.id is now the single attribute every tool uses.

Severity is the reviewer's own estimate and is used for ordering, not filtering. No finding blocks merge automatically; a maintainer decides.

@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Deep Review

Scope: feat(mcp): record which source each MCP tool call reads — 13 production files + 2 test files under packages/api/src/mcp/, against base 8384fe6.
Intent: record the source each MCP tool resolves via a new recordMcpSource helper (mcp.source.id / mcp.source.kind / mcp.source.ids span attributes backed by an AsyncLocalStorage scope), add a bounded source_kind label to the tool duration/error metrics, and wrap source-reading ClickHouse queries in withQueryAttribution({ source }) so log_comment carries the source id.

✅ No critical issues found. The core mechanism is sound and well-guarded. Verified directly: nested withQueryAttribution({ source }) merges with the ambient { surface, label, trace } scope (common-utils/.../node.ts) so each query keeps surface/label/trace while adding its own source; the trace-waterfall log-source query correctly nests a second scope so mcp.source.ids accumulates both while mcp.source.id stays the primary; getSourceKindLabel yields 'none' on an empty Map (safe on the throw path); source_kind is always a bounded SourceKind enum, never a raw id — no metric-cardinality risk; and each tool invocation gets a fresh toolSourceScope.run, so concurrent calls and parallel tiles stay isolated.

🟡 P2 — recommended

  • packages/api/src/mcp/tools/trace/waterfall.ts:674 — the secondary log-source attribution path (two sources recorded on one tool span, the correlated-logs query wrapped in its own withQueryAttribution) has no integration test; only the generic multi-source unit path and runConfigTile are covered end to end, so a regression that drops or mis-scopes this handler's wiring would pass CI.
    • Fix: add a test driving the waterfall handler with includeLogs and a configured logSourceId through withToolTracing, asserting both sources land on mcp.source.ids (trace first, log second), mcp.source.kind stays the trace kind, and the logs query's log_comment carries the log source id.
    • testing
🔵 P3 nitpicks (4)
  • packages/api/src/mcp/tools/sources/describeMetric.ts:500 — the metric source-load + client-lookup + getMetadata preamble is duplicated with listMetrics.ts:153 (the jscpd-flagged clone), and the metric tools have no loadMetricSource helper mirroring the trace tools' loadTraceSource.
    • Fix: extract a loadMetricSource(teamId, sourceId, toolName) helper covering getSource + recordMcpSource + the SourceKind.Metric check + the client lookup, and call it from both handlers.
    • maintainability, previous-comments
  • packages/api/src/mcp/utils/tracing.ts:54 — recordMcpSource runs source._id.toString() and span.setAttribute inside the live tool handler without a guard, unlike the explicitly defensive toStatusMessage; a malformed source or non-standard span introduced by a future refactor would turn a successful tool call into a failure.
    • Fix: wrap the body in a swallow-and-log guard so the "tracing must never fail a tool" invariant is enforced mechanically.
    • reliability
  • .changeset/mcp-source-attribution.md:5 — the summary uses a feat: prefix with a patch bump, while the repo convention pairs feat:→minor and fix:/perf:/chore:→patch.
    • Fix: align the prefix with the patch bump (e.g. perf:/chore:) or raise the bump to minor.
    • project-standards
  • packages/api/src/mcp/tools/trace/breakdown.ts:88 — the trace-tool preamble (parseTimeRange → loadTraceSource → open withQueryAttribution) clone with waterfall.ts:346 grew past jscpd's threshold when this diff added the attribution wrapper.
    • Fix: accept as intentional boilerplate or fold both into a shared trace-tool wrapper; collapsing either side keeps the duplication check green.
    • maintainability

Reviewers (7): testing, maintainability, performance, reliability, kieran-typescript, project-standards, previous-comments. (The correctness and adversarial reviewers were still running at synthesis time; findings reflect the seven that completed plus a direct correctness/concurrency pass over tracing.ts, helpers.ts, shared.ts, and the waterfall/serviceHealth diffs.)

Testing gaps:

  • ~9 instrumented handlers (clickstack_sql, denoise, event_deltas, event_patterns, service_health, describe_metric, describe_source, list_metrics, trace breakdown) have no per-handler test asserting source attribution or log_comment propagation; only runConfigTile is covered end to end, so per-call-site wiring relies on the well-tested mechanism being hand-wired correctly.
  • No test covers the resolved-but-wrong-kind branches (source recorded before the kind check, so source_kind is still emitted on the error counter) or the waterfall attribution-skipping branches (log source not found / wrong kind / connection mismatch).

No finding blocks merge automatically. A maintainer will expect P0/P1 findings in code this PR changes to be fixed; P2/P3 are your call -- fix or reply. Never fix findings about surrounding code here; reply instead. Do not widen the PR. How to respond

@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

E2E Test Results

✅ All tests passed • 462 passed • 1 skipped • 1513s

Status Count
✅ Passed 462
❌ Failed 0
⚠️ Flaky 0
⏭️ Skipped 1

Tests ran across 4 shards in parallel.

View full report →

…t lookup

Folds the source lookup, kind check and client lookup duplicated between the
trace waterfall and breakdown handlers into loadTraceSource, and switches
list_metrics and describe_metric to getMcpClickhouseClient (jscpd).
* Load the trace source a trace tool reads and a client for its connection, or
* the error to return.
*/
export async function loadTraceSource(

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.

🔵 minor — loadTraceSource duplicates loadHealthSource (serviceHealth/source.ts)

loadHealthSource in packages/api/src/mcp/tools/query/serviceHealth/source.ts:53 already does the same thing: getSource, recordMcpSource, check kind === Trace, and build the tool-name error. It also has a better not-found message (sourceNotFound suggests the right ID when the caller passed a name or table). Build loadHealthSource on top of loadTraceSource, or move sourceNotFound into loadTraceSource, so the trace tools share one loader and one not-found message.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

const id = source._id.toString();
if (scope.sources.has(id)) return;
scope.sources.set(id, source.kind);
if (scope.sources.size === 1) {

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.

🔵 minor — Primary source_kind is nondeterministic for clickstack_query_tiles

queryTiles.ts:279 runs runConfigTile for several tiles at once through PQueue. Whichever tile's getSource resolves first becomes mcp.source.id/mcp.source.kind and the metric's source_kind. On a dashboard with both log and trace tiles, the same call can be counted under log on one run and trace on the next. For batch tools, pick the primary source deterministically: record sources in tile order before the tiles run, or use a fixed label such as mixed when more than one kind is seen.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

This branch was successfully deployed

2 active deployments
Preview – hyperdx-oss — f108b8c6 Deployed Nov 3, 2026 by vercel[bot]
Preview – hyperdx-storybook — f108b8c6 Deployed Nov 3, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/tier-4 Critical — deep review + domain expert sign-off

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant