Skip to content

feat(mcp): capture $mcp_client_user_agent and $mcp_vendor_client - #883

Merged
gesh merged 2 commits into
mainfrom
posthog-code/mcp-client-attribution
Aug 21, 2026
Merged

feat(mcp): capture $mcp_client_user_agent and $mcp_vendor_client#883
gesh merged 2 commits into
mainfrom
posthog-code/mcp-client-attribution

Conversation

@gesh

@gesh gesh commented Aug 21, 2026

Copy link
Copy Markdown
Member

Stacked on #882#881. Review those first; this branch's own diff is small.

The problem

clientInfo.name says which client library is calling, not which product. Anthropic reports claude-code from the CLI, the Agent SDK, the VS Code extension and the desktop app alike, so $mcp_client_name collapses every surface into one bucket — which is why the harness breakdown reads 100% "Other" for Python-backed servers.

Reported from the field by a team running a hand-rolled Python dispatcher:

The harness breakdown showed 100 percent "Other" … Sending the raw User-Agent turned out to be the fix, but that should be discoverable from the chart itself.

The change

Property Source
$mcp_client_user_agent the user-agent header, e.g. claude-code/2.1.0 (cli) vs (sdk-ts) vs (claude-vscode)
$mcp_vendor_client the x-anthropic-client header, captured as a second independent signal

Both are captured raw and classified nowhere — friendly names resolve at query time, so labels can improve and new surfaces appear without waiting on an SDK release, and there's one resolver instead of one per installed version. Emitted on every event type that carries client identity.

Read through get_request_headers (added in #881), so it works identically on both SDK majors. HTTP transports only — stdio and in-memory servers carry no headers and their events stay byte-identical.

Custom dispatchers hold their own request object, so every PostHogMCP.capture_* method now takes client_user_agent / vendor_client directly — that's the path the reporting team is on.

Safety

  • Values bounded by the existing metadata cap, so a hostile 1MB header can't inflate an event
  • The read is fully guarded — surface attribution must never break a tool call (tested with a header object that throws)
  • Written per request onto the event, never cached into server-wide state: one server multiplexes concurrent clients, and caching a header would attribute one client's surface to another's event

Also unifies how the v2 adapter reaches the request context — it read the private _request_context while v1 reads the public property. Both work; the guarded public read removes a private-attribute dependency and the asymmetry.

Tests

9 new, both majors: both headers stamped, case-insensitivity, each header independent, stdio/no-ctx stamping nothing, a throwing header object, an end-to-end instrumented server, custom dispatchers passing their own, absent headers leaving events byte-identical, and a 10KB header being bounded.

Suite: 206 → 215 (v1) · 191 → 200 (v2). ruff, mypy, public-API snapshot clean.


Created with PostHog Code

@gesh
gesh requested a review from a team as a code owner August 21, 2026 09:13
@greptile-apps

greptile-apps Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Reviews (1): Last reviewed commit: "feat(mcp): capture $mcp_client_user_agen..." | Re-trigger Greptile

@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

posthog-python Compliance Report

Date: 2026-08-21 12:51:58 UTC
Duration: 256460ms

✅ All Tests Passed!

111/111 tests passed


Capture_V1 Tests

94/94 tests passed

View Details
Test Status Duration
Endpoint And Method.Targets V1 Endpoint 517ms
Endpoint And Method.Does Not Use Legacy Endpoints 511ms
Required Headers.Has Authorization Bearer Header 511ms
Required Headers.Has Content Type Json 510ms
Required Headers.Has Posthog Sdk Info Format 510ms
Required Headers.Has Posthog Attempt Header 510ms
Required Headers.Has Posthog Request Id 510ms
Required Headers.Has Posthog Request Timestamp 511ms
Required Headers.Has User Agent 511ms
Body Format.Body Has Created At And Batch 510ms
Body Format.No Api Key In Body 510ms
Body Format.No Sent At In Body 510ms
Event Format.Event Has Required Root Fields 511ms
Event Format.Event Uuid Is Valid 512ms
Event Format.Event Timestamp Is Rfc3339 510ms
Event Format.Distinct Id Is String 511ms
Event Format.Distinct Id At Root Not Properties 511ms
Event Format.Custom Properties Preserved 510ms
Event Format.Set Properties Preserved 511ms
Event Format.Set Once Properties Preserved 510ms
Event Format.Groups Properties Preserved 510ms
Event Format.Sdk Generates Uuid If Not Provided 510ms
Event Format.Event Has Required Root Fields Batch 515ms
Event Format.Event Uuid Is Valid Batch 514ms
Event Format.Event Timestamp Is Rfc3339 Batch 514ms
Event Format.Distinct Id Is String Batch 514ms
Event Format.Distinct Id At Root Not Properties Batch 514ms
Event Format.Custom Properties Preserved Batch 514ms
Event Format.Set Properties Preserved Batch 514ms
Event Format.Set Once Properties Preserved Batch 513ms
Event Format.Groups Properties Preserved Batch 514ms
Event Format.Sdk Generates Uuid If Not Provided Batch 513ms
Batch Behavior.Multiple Events In Single Batch 518ms
Batch Behavior.Batch Envelope Smoke 516ms
Batch Behavior.Flush With No Events Sends Nothing 507ms
Batch Behavior.Flush At Triggers Batch 1011ms
Batch Behavior.Created At Reflects Batch Creation Time 512ms
Deduplication.Generates Unique Uuids 517ms
Deduplication.Different Events Same Content Different Uuids 513ms
Deduplication.Preserves Uuid On Retry 6519ms
Deduplication.Preserves Timestamp On Retry 6519ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry 6524ms
Deduplication.No Duplicate Events In Batch 519ms
Header Behavior On Retry.Attempt Header Starts At One 510ms
Header Behavior On Retry.Attempt Header Increments On Retry 13521ms
Header Behavior On Retry.Request Id Preserved On Retry 6516ms
Header Behavior On Retry.Different Requests Have Different Request Ids 3020ms
Header Behavior On Retry.Request Timestamp Changes On Retry 6520ms
Response Format Validation.Success Response Has Uuid Keyed Results 511ms
Response Format Validation.Success Response Has Ok For Each Event 515ms
Response Format Validation.Success No Retry After When All Ok 513ms
Response Format Validation.Success Retry After Present When Retry Events 1516ms
Response Format Validation.Success No Retry After When Drop Only 513ms
Response Format Validation.Response Echoes Request Id 510ms
Retry Behavior.Retries On 408 6521ms
Retry Behavior.Retries On 500 6515ms
Retry Behavior.Retries On 503 8524ms
Retry Behavior.Retries On 504 6521ms
Retry Behavior.Retryable Errors Have Retry After 3517ms
Retry Behavior.Respects Retry After On Retryable Error 11523ms
Retry Behavior.Does Not Retry On 400 2513ms
Retry Behavior.Does Not Retry On 401 2513ms
Retry Behavior.Does Not Retry On 402 2513ms
Retry Behavior.Does Not Retry On 413 2513ms
Retry Behavior.Does Not Retry On 415 2514ms
Retry Behavior.Non Retryable Errors Have No Retry After 2514ms
Retry Behavior.Implements Backoff 22534ms
Retry Behavior.Max Retries Respected 22537ms
Partial Batch Handling.Handles 200 Full Success 2512ms
Partial Batch Handling.Handles 200 With All Ok 3515ms
Partial Batch Handling.Does Not Retry Dropped Events 3516ms
Partial Batch Handling.Does Not Retry Limited Events 3516ms
Partial Batch Handling.Prunes Ok Events On Partial Retry 6521ms
Partial Batch Handling.Prunes Dropped Events On Partial Retry 6522ms
Partial Batch Handling.Retries Only Retry Events From Partial 6523ms
Partial Batch Handling.Partial Retry Preserves Uuids 6522ms
Partial Batch Handling.Partial Retry Attempt Header Increments 6522ms
Partial Batch Handling.Partial Retry Request Id Preserved 6522ms
Partial Batch Handling.Respects Retry After On Partial 8520ms
Partial Batch Handling.Unknown Result Treated As Terminal 3515ms
Partial Batch Handling.Mixed Ok Drop Limited No Retry 3519ms
Compression.Sends Gzip Content Encoding 512ms
Compression.No Content Encoding When Disabled 510ms
Compression.Compressed Body Is Decompressible 511ms
Error Handling.Does Not Retry On Unknown 4Xx 2512ms
Event Options.Cookieless Mode Override 511ms
Event Options.Disable Skew Correction Override 512ms
Event Options.Process Person Profile Override 510ms
Event Options.Product Tour Id Override 510ms
Event Options.Unset Options Omitted 511ms
Event Options.Options Override In Batch 514ms
Geoip And Historical Migration.Geoip Disable Injected Into Properties 511ms
Geoip And Historical Migration.Historical Migration Set In Body 510ms
Geoip And Historical Migration.Historical Migration Absent By Default 510ms

Feature_Flags Tests

17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id 11ms
Request Payload.Flags Request Uses V2 Query Param 10ms
Request Payload.Flags Request Hits Flags Path Not Decide 9ms
Request Payload.Flags Request Omits Authorization Header 11ms
Request Payload.Token In Flags Body Matches Init 9ms
Request Payload.Groups Round Trip 10ms
Request Payload.Groups Default To Empty Object 10ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False 10ms
Request Payload.Disable Geoip Omitted Defaults To False 9ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key 10ms
Request Lifecycle.No Flags Request On Init Alone 4ms
Request Lifecycle.No Flags Request On Normal Capture 509ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests 14ms
Request Lifecycle.Mock Response Value Is Returned To Caller 10ms
Retry Behavior.Retries Flags On 502 313ms
Retry Behavior.Retries Flags On 504 313ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event 512ms

@gesh
gesh requested a review from a team August 21, 2026 09:23
Base automatically changed from posthog-code/mcp-error-properties to main August 21, 2026 12:34
gesh added 2 commits August 21, 2026 15:45
clientInfo.name says which client *library* is calling, not which product.
Anthropic reports "claude-code" from the CLI, the Agent SDK, the VS Code
extension and the desktop app alike, so $mcp_client_name collapses every
surface into one bucket — which is why the harness breakdown reads 100%
"Other" for Python-backed servers, reported from the field.

The distinguishing detail lives in the User-Agent parenthetical
(claude-code/2.1.0 (cli) vs (sdk-ts) vs (claude-vscode)) and in vendor
headers like x-anthropic-client. Both are captured raw and classified
nowhere: friendly names resolve at query time, so labels improve and new
surfaces appear without waiting on an SDK release, and there is one
resolver rather than one per installed version.

Read through get_request_headers, so it works identically on both SDK
majors; HTTP transports only, so stdio and in-memory events stay
byte-identical. Values are bounded by the existing metadata cap, so a
hostile header cannot inflate an event, and the read is fully guarded —
surface attribution must never break a tool call. Custom dispatchers hold
their own request object, so every PostHogMCP.capture_* method takes
client_user_agent / vendor_client directly.

Also unifies how the v2 adapter reaches the request context: it read the
private _request_context while v1 reads the public property. Both work,
but the public read (guarded, since it raises outside a request) removes a
private-attribute dependency and the asymmetry.

Parity with @posthog/mcp's transport-identity module.

Generated-By: PostHog Code
Task-Id: ebafcb71-b03b-443d-b40c-d527ed4a04f4
The unit tests drove a hand-built headers mapping, which proves the
function works but not the feature: the whole point is reading headers off
a real request, and "the User-Agent never showed up" is the production
symptom this closes. Both new tests send a real User-Agent and
X-Anthropic-Client through an actual streamable-HTTP app and assert the
properties land on the captured event:

- v2: through the existing dual-era httpx/ASGITransport harness
- v1: through a real FastMCP app via Starlette's TestClient, reusing the
  pattern in test_session_token.py — v1 is where every MCP client today
  still lives, so it is the lane that most needs the real-transport proof

Both exercise Starlette's own Headers object rather than a dict, which is
the shape get_request_headers actually meets in production.

Generated-By: PostHog Code
Task-Id: ebafcb71-b03b-443d-b40c-d527ed4a04f4
@gesh
gesh force-pushed the posthog-code/mcp-client-attribution branch from 63f8f93 to 9825917 Compare August 21, 2026 12:46
@gesh
gesh merged commit f483bab into main Aug 21, 2026
42 checks passed
@gesh
gesh deleted the posthog-code/mcp-client-attribution branch August 21, 2026 12:54
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.

2 participants