Skip to content

Commit 9c932be

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 171af6b commit 9c932be

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
@@ -19,6 +19,7 @@ You need to change code if your app does any of these:
1919
- sets `$is_server`, `$geoip_disable` or a system property such as `$os` and expects the SDK's value to win
2020
- tests the AI integrations with a mock client and asserts on `capture`
2121
- passes its own client object to the AI integrations
22+
- sets `$lib` or `$lib_version`, or filters on `$lib = "posthog-python-mcp"`
2223

2324
## Endpoints
2425

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

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

3548
| Removed | Use instead |

‎posthog/client.py‎

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,11 @@
101101
from posthog.poller import Poller
102102
from posthog.release_id import _resolve_release_id
103103
from posthog.request import (
104-
USER_AGENT as _USER_AGENT,
105104
APIError,
106105
DatetimeSerializer as _DatetimeSerializer,
107106
QuotaLimitError,
108107
RequestsConnectionError,
109108
RequestsTimeout,
110-
_get as _get_with_identity,
111-
_remote_config as _remote_config_with_identity,
112109
determine_server_host,
113110
flags,
114111
get,
@@ -1198,19 +1195,6 @@ def __init__(
11981195

11991196
self._warn_if_duplicate_async_client()
12001197

1201-
def _set_library_identity(self, library_id: str, library_version: str) -> None:
1202-
"""Override the SDK identity stamped on events and outbound requests."""
1203-
self._library_id = library_id
1204-
self._library_version = library_version
1205-
self._sdk_info = f"{library_id}/{library_version}"
1206-
for lane in self._lanes:
1207-
lane.sdk_info = self._sdk_info
1208-
for consumer in lane.consumers:
1209-
consumer._sdk_info = self._sdk_info
1210-
1211-
def _request_identity_kwargs(self) -> Dict[str, str]:
1212-
return {"_user_agent": self._sdk_info} if self._sdk_info != _USER_AGENT else {}
1213-
12141198
@property
12151199
def queue(self) -> Queue:
12161200
"""The analytics lane's queue (kept for backwards compatibility)."""
@@ -1664,8 +1648,6 @@ def _get_flags_decision(
16641648

16651649
if flag_keys_to_evaluate:
16661650
request_data["flag_keys_to_evaluate"] = flag_keys_to_evaluate
1667-
if self._sdk_info != _USER_AGENT:
1668-
request_data["_user_agent"] = self._sdk_info
16691651

16701652
resp_data = flags(
16711653
self.api_key,
@@ -3474,14 +3456,12 @@ def _fetch_feature_flags_from_api(self):
34743456
cache_data_to_store: Optional[FlagDefinitionCacheData] = None
34753457
old_fingerprint: Optional[str] = None
34763458
try:
3477-
request_get = _get_with_identity if self._request_identity_kwargs() else get
3478-
response = request_get(
3459+
response = get(
34793460
personal_api_key,
34803461
f"/flags/definitions?token={self.api_key}&send_cohorts",
34813462
self.host,
34823463
timeout=10,
34833464
etag=request_etag,
3484-
**self._request_identity_kwargs(),
34853465
)
34863466

34873467
# Canonical serialization can be expensive; do it before publication.
@@ -4465,18 +4445,12 @@ def get_remote_config_payload(self, key: str):
44654445
return None
44664446

44674447
try:
4468-
request_remote_config = (
4469-
_remote_config_with_identity
4470-
if self._request_identity_kwargs()
4471-
else remote_config
4472-
)
4473-
return request_remote_config(
4448+
return remote_config(
44744449
self.personal_api_key,
44754450
self.api_key,
44764451
self.host,
44774452
key,
44784453
timeout=self.feature_flags_request_timeout_seconds,
4479-
**self._request_identity_kwargs(),
44804454
)
44814455
except Exception as e:
44824456
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)