Skip to content

Commit 44123ba

Browse files
committed
feat!: keep the posthog-python identity when instrumenting MCP
MCP instrumentation relabeled the whole client, so the app's own events and flag requests reported posthog-python-mcp. Capture v1 takes $lib from the per-request PostHog-Sdk-Info header, which is now always posthog-python.
1 parent e129027 commit 44123ba

11 files changed

Lines changed: 34 additions & 139 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
pypi/posthog: major
3+
---
4+
5+
MCP instrumentation no longer relabels the client as `posthog-python-mcp`. `posthog.mcp.instrument()` and `PostHogMCP` used to change `$lib`, the `PostHog-Sdk-Info` header and the feature flag request `User-Agent` for every event the client sent, including the app's own events. All events now report `posthog-python`. Filter MCP traffic on the `$mcp_*` events and properties.

‎docs/migration-7.x-to-8.0.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ You need to change code if your app does any of these:
1818
- expects `super_properties` to override properties passed to a single call
1919
- tests the AI integrations with a mock client and asserts on `capture`
2020
- passes its own client object to the AI integrations
21+
- sets `$lib` or `$lib_version`, or filters on `$lib = "posthog-python-mcp"`
2122

2223
## Endpoints
2324

@@ -29,6 +30,18 @@ You need to change code if your app does any of these:
2930
If you send events to a self-hosted PostHog, check that it serves both endpoints before you upgrade.
3031
An endpoint that is not served drops every event sent to it.
3132

33+
## SDK identity
34+
35+
PostHog sets `$lib` and `$lib_version` on every event from the `PostHog-Sdk-Info` request header, which is always `posthog-python/<version>`.
36+
The SDK removes `$lib` and `$lib_version` from the properties it sends.
37+
A value you set in a call, in `super_properties` or in `before_send` does not reach PostHog.
38+
39+
MCP instrumentation no longer relabels the client.
40+
In 7.x, `posthog.mcp.instrument()` and `PostHogMCP` set the client's identity to `posthog-python-mcp`.
41+
With the default client, or any client the app also used, every event and feature flag request from the app then reported `posthog-python-mcp`, not only the MCP events.
42+
In 8.0, MCP events report `posthog-python` like all other events.
43+
To find MCP traffic, filter on the `$mcp_*` events and properties instead of `$lib`.
44+
3245
## Removed options and APIs
3346

3447
| Removed | Use instead |

‎posthog/client.py‎

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -94,14 +94,11 @@
9494
from posthog.poller import Poller
9595
from posthog.release_id import _resolve_release_id
9696
from posthog.request import (
97-
USER_AGENT as _USER_AGENT,
9897
APIError,
9998
DatetimeSerializer as _DatetimeSerializer,
10099
QuotaLimitError,
101100
RequestsConnectionError,
102101
RequestsTimeout,
103-
_get as _get_with_identity,
104-
_remote_config as _remote_config_with_identity,
105102
determine_server_host,
106103
flags,
107104
get,
@@ -1162,19 +1159,6 @@ def __init__(
11621159

11631160
self._warn_if_duplicate_async_client()
11641161

1165-
def _set_library_identity(self, library_id: str, library_version: str) -> None:
1166-
"""Override the SDK identity stamped on events and outbound requests."""
1167-
self._library_id = library_id
1168-
self._library_version = library_version
1169-
self._sdk_info = f"{library_id}/{library_version}"
1170-
for lane in self._lanes:
1171-
lane.sdk_info = self._sdk_info
1172-
for consumer in lane.consumers:
1173-
consumer._sdk_info = self._sdk_info
1174-
1175-
def _request_identity_kwargs(self) -> Dict[str, str]:
1176-
return {"_user_agent": self._sdk_info} if self._sdk_info != _USER_AGENT else {}
1177-
11781162
@property
11791163
def queue(self) -> Queue:
11801164
"""The analytics lane's queue (kept for backwards compatibility)."""
@@ -1626,8 +1610,6 @@ def _get_flags_decision(
16261610

16271611
if flag_keys_to_evaluate:
16281612
request_data["flag_keys_to_evaluate"] = flag_keys_to_evaluate
1629-
if self._sdk_info != _USER_AGENT:
1630-
request_data["_user_agent"] = self._sdk_info
16311613

16321614
resp_data = flags(
16331615
self.api_key,
@@ -3436,14 +3418,12 @@ def _fetch_feature_flags_from_api(self):
34363418
cache_data_to_store: Optional[FlagDefinitionCacheData] = None
34373419
old_fingerprint: Optional[str] = None
34383420
try:
3439-
request_get = _get_with_identity if self._request_identity_kwargs() else get
3440-
response = request_get(
3421+
response = get(
34413422
personal_api_key,
34423423
f"/flags/definitions?token={self.api_key}&send_cohorts",
34433424
self.host,
34443425
timeout=10,
34453426
etag=request_etag,
3446-
**self._request_identity_kwargs(),
34473427
)
34483428

34493429
# Canonical serialization can be expensive; do it before publication.
@@ -4427,18 +4407,12 @@ def get_remote_config_payload(self, key: str):
44274407
return None
44284408

44294409
try:
4430-
request_remote_config = (
4431-
_remote_config_with_identity
4432-
if self._request_identity_kwargs()
4433-
else remote_config
4434-
)
4435-
return request_remote_config(
4410+
return remote_config(
44364411
self.personal_api_key,
44374412
self.api_key,
44384413
self.host,
44394414
key,
44404415
timeout=self.feature_flags_request_timeout_seconds,
4441-
**self._request_identity_kwargs(),
44424416
)
44434417
except Exception as e:
44444418
self.log.exception(

‎posthog/mcp/README.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ analytics = instrument(server, posthog)
3131
Install is just `pip install posthog`. `instrument()` needs the MCP SDK at runtime,
3232
but anyone wrapping a server already has it.
3333

34-
MCP analytics events report `$lib: "posthog-python-mcp"` and the installed `posthog` package version in `$lib_version`.
35-
Request headers use the same identity and package version, so SDK Health can compare MCP traffic with Python SDK releases.
36-
Because `$lib` is a client-level identity, `instrument()` relabels every event sent by the client passed to it.
34+
MCP analytics events report `$lib: "posthog-python"` and the installed `posthog` package version in `$lib_version`, like every other event the client sends.
35+
PostHog sets both from the client's `PostHog-Sdk-Info` request header, so `instrument()` and `PostHogMCP` do not relabel the client.
36+
Filter MCP traffic by its `$mcp_*` events and properties instead.
3737
Use a client dedicated to MCP analytics if the application also captures unrelated events.
3838

3939
## Defaults and opt-outs

‎posthog/mcp/__init__.py‎

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -351,11 +351,6 @@ def instrument(
351351
"on SDK 2.x, or jlowin's fastmcp.FastMCP) or a low-level mcp.server.Server."
352352
)
353353

354-
if client is not None:
355-
from ._lib_identity import apply_mcp_lib_identity
356-
357-
apply_mcp_lib_identity(client)
358-
359354
# Zero-config stateless minting: wrap the server's ASGI-app factories so a
360355
# stateless/multi-pod deployment keeps one $session_id + the client harness
361356
# across pods with no extra setup. No-op for stdio / low-level servers.

‎posthog/mcp/_lib_identity.py‎

Lines changed: 0 additions & 15 deletions
This file was deleted.

‎posthog/mcp/constants.py‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343
)
4444

4545
POSTHOG_MCP_ANALYTICS_SOURCE = "posthog_mcp_analytics"
46-
POSTHOG_MCP_LIB_NAME = "posthog-python-mcp"
4746

4847

4948
class PostHogMCPAnalyticsEvent:

‎posthog/mcp/posthog_mcp.py‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@
4242
fire_and_forget,
4343
virtual_tool_collision_message,
4444
)
45-
from ._lib_identity import apply_mcp_lib_identity
4645
from .logger import log, warn
4746
from ._model_parameters import (
4847
add_model_parameter_to_schema,
@@ -122,7 +121,6 @@ def __init__(
122121
) -> None:
123122
self._server_build = validate_server_build(server_build)
124123
super().__init__(api_key, **kwargs)
125-
apply_mcp_lib_identity(self)
126124
self._mcp_sink = McpEventSink(self)
127125
self._missing_capability_tool_name = (
128126
missing_capability_tool_name or _GET_MORE_TOOLS_NAME

‎posthog/test/mcp/test_no_crash.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def test_low_level_server_detected_on_installed_major():
6161
assert compat.is_low_level_server(object()) is False
6262

6363

64-
def test_instrument_relabels_the_host_client():
64+
def test_instrument_keeps_the_host_client_identity():
6565
from mcp.server.lowlevel import Server
6666

6767
captured = []
@@ -74,7 +74,7 @@ def before_send(event):
7474
instrument(Server("probe-lib-identity"), client)
7575
client.capture("after instrumentation")
7676

77-
assert captured[0]["properties"]["$lib"] == "posthog-python-mcp"
77+
assert captured[0]["properties"]["$lib"] == "posthog-python"
7878
assert captured[0]["properties"]["$lib_version"] == VERSION
7979

8080

‎posthog/test/mcp/test_posthog_mcp.py‎

Lines changed: 2 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -109,86 +109,12 @@ def before_send(event):
109109
assert captured[2]["properties"]["$mcp_server_build"] == "default-build"
110110

111111

112-
async def test_mcp_events_use_mcp_library_identity():
113-
captured = []
114-
115-
def before_send(event):
116-
captured.append(event)
117-
return event
118-
119-
client = PostHogMCP(
120-
"phc_test",
121-
host="https://us.i.posthog.com",
122-
send=False,
123-
before_send=before_send,
124-
)
125-
client.capture_tool_call("broken", is_error=True, error=RuntimeError("kaboom"))
126-
await _flush()
127-
128-
assert {event["event"] for event in captured} == {"$mcp_tool_call", "$exception"}
129-
assert all(
130-
event["properties"]["$lib"] == "posthog-python-mcp"
131-
and event["properties"]["$lib_version"] == VERSION
132-
for event in captured
133-
)
134-
135-
136-
def test_mcp_library_identity_reaches_capture_v1_header():
112+
def test_mcp_client_keeps_the_sdk_identity():
137113
client = PostHogMCP("phc_test", sync_mode=True)
138114
with mock.patch("posthog.client._send_v1_batch") as send:
139115
client.capture("$mcp_custom")
140116

141-
assert send.call_args.kwargs["sdk_info"] == f"posthog-python-mcp/{VERSION}"
142-
event = send.call_args.args[2][0]
143-
assert event["properties"]["$lib"] == "posthog-python-mcp"
144-
assert event["properties"]["$lib_version"] == VERSION
145-
146-
147-
def test_mcp_library_identity_reaches_feature_flag_requests():
148-
response = mock.Mock(status_code=200)
149-
response.json.return_value = {"flags": {}}
150-
client = PostHogMCP("phc_test", send=False)
151-
152-
with mock.patch(
153-
"posthog.request._flags_session.post", return_value=response
154-
) as post:
155-
client.evaluate_flags("user_1")
156-
157-
assert post.call_args.kwargs["headers"]["User-Agent"] == (
158-
f"posthog-python-mcp/{VERSION}"
159-
)
160-
161-
162-
def test_mcp_library_identity_reaches_feature_flag_definition_requests():
163-
response = mock.Mock(status_code=200, headers={})
164-
response.json.return_value = {"flags": [], "group_type_mapping": {}, "cohorts": {}}
165-
client = PostHogMCP(
166-
"phc_test",
167-
secret_key="phs_test",
168-
send=False,
169-
enable_local_evaluation=False,
170-
)
171-
172-
with mock.patch("posthog.request._session.get", return_value=response) as get:
173-
client.load_feature_flags()
174-
175-
assert get.call_args.kwargs["headers"]["User-Agent"] == (
176-
f"posthog-python-mcp/{VERSION}"
177-
)
178-
client.shutdown()
179-
180-
181-
def test_mcp_library_identity_reaches_remote_config_requests():
182-
response = mock.Mock(status_code=200, headers={})
183-
response.json.return_value = "payload"
184-
client = PostHogMCP("phc_test", secret_key="phs_test", send=False)
185-
186-
with mock.patch("posthog.request._session.get", return_value=response) as get:
187-
assert client.get_remote_config_payload("flag-key") == "payload"
188-
189-
assert get.call_args.kwargs["headers"]["User-Agent"] == (
190-
f"posthog-python-mcp/{VERSION}"
191-
)
117+
assert send.call_args.kwargs["sdk_info"] == f"posthog-python/{VERSION}"
192118

193119

194120
async def test_capture_initialize_and_tools_list():

0 commit comments

Comments
 (0)