Skip to content

MCP: query attribution in log_comment can't tell users, API keys or sessions apart #3540

Closed

Description

@bcanvural

Summary

Every ClickHouse query an MCP tool sends carries log_comment = {"surface":"mcp","label":<tool>,"trace":<id>} (packages/api/src/mcp/utils/tracing.ts:113-120). That is very useful: we used it to find the cost of every MCP call in system.query_log.

But the user, the API key and the MCP client/session are only on the span and in the API log, not in log_comment (QueryAttribution in packages/common-utils/src/clickhouse/attribution.ts:49 has no field for them). So when several agents or people use the MCP at the same time, system.query_log alone can't say which session sent a query.

Environment

HyperDX 2-nightly image (late Oct 2026) with ClickHouse 26.10.3. Code references checked on main at d08b2fa (2026-11-03, 2.43.1).

Steps to reproduce

  1. Run two or more MCP clients at the same time, with the same or different API keys.
  2. Look at their queries in ClickHouse:
    SELECT event_time, query_duration_ms, read_bytes, log_comment
    FROM system.query_log
    WHERE type = 'QueryFinish' AND JSONExtractString(log_comment, 'surface') = 'mcp'
    ORDER BY event_time DESC LIMIT 20
  3. All rows share surface: "mcp". Only the per-call trace differs, and mapping it back to a user or session needs a join to the API's own log.

Expected vs actual

Expected Actual
Who sent an MCP query, from query_log alone user (or API key id) and MCP session / client in log_comment only surface, label, trace

Impact

In one hour, about 10 agents shared the MCP while investigating production logs. In the same minute, single calls read 34 GiB and 53 GiB. Finding out which session sent which expensive query meant joining query_log.log_comment.trace to the API log for every call. With the session in log_comment this is one GROUP BY, and it also opens the door to per-session limits on the ClickHouse side.

Proposed fix

Add to log_comment for the MCP surface:

  • the user id, or the API key id once keys can be team-owned (feat: team-owned API keys #3435);
  • the MCP session id (the transport's Mcp-Session-Id) and the client name/version from initialize (already on the span as mcp.client.name).

Optional follow-up: pass a per-user or per-session quota_key, so operators can set ClickHouse quotas (bytes read, queries) for MCP sessions.

Happy to send a PR for the log_comment fields.

Related

Activity

  1. brandon-pereira commented on Nov 4, 2026

    @brandon-pereira
    Member

    Makes sense, open to a contribution for this change. Thanks for raising the issue :)

  2. bcanvural commented on Nov 4, 2026

    @bcanvural
    ContributorAuthor

    Thanks! Before I open a PR, one design question, because it touches a rule in attribution.ts (from #3155):

    Records what was asked for, never who asked. No team, user or session id goes in here.

    What we need is to tell apart the MCP calls of different people: several people's agents use one HyperDX, and they all send the same client string (e.g. claude-code/2.1.215), so the client name alone doesn't separate them. A session id isn't available either: the MCP endpoint is stateless (sessionIdGenerator: undefined).

    Two ways to do it, both next to a client field (name/version from the User-Agent):

    1. Add the user id for the MCP surface, i.e. the HyperDX user's id (not the e-mail), and relax the rule for MCP. It's the simplest, and system.query_log is normally readable only by ClickHouse operators. They can map the id to a user in HyperDX.
    2. Keep the rule and add an opaque caller instead: HMAC-SHA256 of the user id, keyed with a sub-key derived from an existing server secret, truncated to 16 hex characters. The same person always gets the same value and different people get different ones, but it can't be reversed from query_log alone; mapping it back needs HyperDX's database and the secret. If no secret is configured, the field is left out.

    With either, cost per person in query_log is one GROUP BY, and a per-caller quota_key becomes possible later.

    We'd lean towards 1, but are happy to do 2 if you'd rather keep the rule as it is. Which do you prefer?

  3. brandon-pereira commented on Nov 4, 2026

    @brandon-pereira
    Member

    Gotcha, I didn't realize that constraint. Good flag. Based on the comment in the file, we should not expose either option. We can add an additional log comment signifying that it came from the MCP server, and what tool ran the query, but we should keep user and team data out as suggested

  4. bcanvural commented on Nov 4, 2026

    @bcanvural
    ContributorAuthor

    Thanks, makes sense. surface: "mcp" and label: <tool> are already there (#3155), so the only addition left is the client name/version (client: "claude-code/2.1.215"), which is "what asked", not "who". Happy to send that small PR if it's useful; otherwise we can close this. We'll do the per-person mapping on our side.

  5. brandon-pereira commented on Nov 4, 2026

    @brandon-pereira
    Member

    Lets close for now. We can revise this in the future if others see a need. Thanks for investigating this!

  6. bcanvural commented on Nov 4, 2026

    @bcanvural
    ContributorAuthor

    Thank you! Also thanks for vouching for me!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions