Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .sampo/changesets/mcp-protocol-version.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
pypi/posthog: minor
---

feat(mcp): emit `$mcp_protocol_version` on MCP analytics events — the MCP spec version, recovered from the session token across stateless pods (parity with the TypeScript SDK). `PostHogMCP` capture methods gain a `protocol_version` argument.
1 change: 1 addition & 0 deletions posthog/mcp/_capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def capture_event(
"server_version": data.server_version,
"client_name": event_input.get("client_name"),
"client_version": event_input.get("client_version"),
"protocol_version": event_input.get("protocol_version"),
"identify_actor_given_id": actor.distinct_id if actor else None,
"identify_actor_data": (actor.properties or {}) if actor else {},
"groups": actor.groups if actor else None,
Expand Down
42 changes: 38 additions & 4 deletions posthog/mcp/_instrument_fastmcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,12 @@ async def wrapped(
convert_result: bool = False,
) -> Any:
client_name, client_version = _client_info(context)
protocol_version = _protocol_version(context)
mcp_session_id = _mcp_session_id(context)
token, client_name, client_version = resolve_session_and_client(
mcp_session_id, client_name, client_version
token, client_name, client_version, protocol_version = (
resolve_session_and_client(
mcp_session_id, client_name, client_version, protocol_version
)
)
request = build_tool_call_request(name, arguments)
extra: Dict[str, Any] = {"session_id": mcp_session_id}
Expand All @@ -103,6 +106,7 @@ async def wrapped(
mcp_session_id=mcp_session_id,
client_name=client_name,
client_version=client_version,
protocol_version=protocol_version,
request=request,
extra=extra,
token=token,
Expand All @@ -118,6 +122,7 @@ async def wrapped(
arguments=arguments,
client_name=client_name,
client_version=client_version,
protocol_version=protocol_version,
extra=extra,
)
return [
Expand Down Expand Up @@ -162,6 +167,7 @@ async def wrapped(
duration_ms=(time.monotonic() - start) * 1000,
client_name=client_name,
client_version=client_version,
protocol_version=protocol_version,
conversation_id=None if minted else conversation_id,
extra=extra,
)
Expand All @@ -187,6 +193,7 @@ async def wrapped(
duration_ms=(time.monotonic() - start) * 1000,
client_name=client_name,
client_version=client_version,
protocol_version=protocol_version,
conversation_id=delivered_conversation_id,
extra=extra,
)
Expand Down Expand Up @@ -215,9 +222,12 @@ async def list_handler(req: Any) -> Any:
return await original(req)

client_name, client_version = _low_level_client_info(server)
protocol_version = _low_level_protocol_version(server)
mcp_session_id = _low_level_session_id(server)
token, client_name, client_version = resolve_session_and_client(
mcp_session_id, client_name, client_version
token, client_name, client_version, protocol_version = (
resolve_session_and_client(
mcp_session_id, client_name, client_version, protocol_version
)
)
request = request_to_dict(req)
extra: Dict[str, Any] = {"session_id": mcp_session_id}
Expand All @@ -228,6 +238,7 @@ async def list_handler(req: Any) -> Any:
mcp_session_id=mcp_session_id,
client_name=client_name,
client_version=client_version,
protocol_version=protocol_version,
request=request,
extra=extra,
token=token,
Expand All @@ -247,6 +258,7 @@ async def list_handler(req: Any) -> Any:
error=error,
client_name=client_name,
client_version=client_version,
protocol_version=protocol_version,
extra=extra,
)
raise
Expand Down Expand Up @@ -299,6 +311,7 @@ async def list_handler(req: Any) -> Any:
error="tools/list returned no tools" if empty else None,
client_name=client_name,
client_version=client_version,
protocol_version=protocol_version,
extra=extra,
)

Expand Down Expand Up @@ -389,6 +402,17 @@ def _low_level_session_id(server: Any) -> Optional[str]:
return None


def _low_level_protocol_version(server: Any) -> Optional[str]:
ctx = _low_level_request_context(server)
try:
client_params = ctx.session.client_params
if client_params:
return client_params.protocolVersion
except Exception: # noqa: BLE001
pass
return None


def _client_info(context: Any) -> Tuple[Optional[str], Optional[str]]:
try:
client_params = context.request_context.session.client_params
Expand All @@ -399,6 +423,16 @@ def _client_info(context: Any) -> Tuple[Optional[str], Optional[str]]:
return None, None


def _protocol_version(context: Any) -> Optional[str]:
try:
client_params = context.request_context.session.client_params
if client_params:
return client_params.protocolVersion
except Exception: # noqa: BLE001
pass
return None


def _mcp_session_id(context: Any) -> Optional[str]:
"""Best-effort transport session id (e.g. the ``Mcp-Session-Id`` header on the
streamable-HTTP transport). Returns ``None`` for stdio, where the SDK-generated
Expand Down
32 changes: 28 additions & 4 deletions posthog/mcp/_instrument_lowlevel.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,12 @@ async def handler(req: Any) -> Any:
name = req.params.name
arguments = dict(req.params.arguments or {})
client_name, client_version = _client_info(server)
protocol_version = _protocol_version(server)
mcp_session_id = _mcp_session_id(server)
token, client_name, client_version = resolve_session_and_client(
mcp_session_id, client_name, client_version
token, client_name, client_version, protocol_version = (
resolve_session_and_client(
mcp_session_id, client_name, client_version, protocol_version
)
)
request = build_tool_call_request(name, arguments)
extra = {"session_id": mcp_session_id}
Expand All @@ -106,6 +109,7 @@ async def handler(req: Any) -> Any:
mcp_session_id=mcp_session_id,
client_name=client_name,
client_version=client_version,
protocol_version=protocol_version,
request=request,
extra=extra,
token=token,
Expand All @@ -121,6 +125,7 @@ async def handler(req: Any) -> Any:
arguments=arguments,
client_name=client_name,
client_version=client_version,
protocol_version=protocol_version,
extra=extra,
)
return mcp_types.ServerResult(
Expand Down Expand Up @@ -169,6 +174,7 @@ async def handler(req: Any) -> Any:
duration_ms=(time.monotonic() - start) * 1000,
client_name=client_name,
client_version=client_version,
protocol_version=protocol_version,
conversation_id=None if minted else conversation_id,
extra=extra,
)
Expand Down Expand Up @@ -203,6 +209,7 @@ async def handler(req: Any) -> Any:
duration_ms=duration_ms,
client_name=client_name,
client_version=client_version,
protocol_version=protocol_version,
conversation_id=delivered_conversation_id,
extra=extra,
)
Expand All @@ -227,9 +234,12 @@ async def handler(req: Any) -> Any:
return await original(req)

client_name, client_version = _client_info(server)
protocol_version = _protocol_version(server)
mcp_session_id = _mcp_session_id(server)
token, client_name, client_version = resolve_session_and_client(
mcp_session_id, client_name, client_version
token, client_name, client_version, protocol_version = (
resolve_session_and_client(
mcp_session_id, client_name, client_version, protocol_version
)
)
request = request_to_dict(req)
extra = {"session_id": mcp_session_id}
Expand All @@ -240,6 +250,7 @@ async def handler(req: Any) -> Any:
mcp_session_id=mcp_session_id,
client_name=client_name,
client_version=client_version,
protocol_version=protocol_version,
request=request,
extra=extra,
token=token,
Expand All @@ -259,6 +270,7 @@ async def handler(req: Any) -> Any:
error=error,
client_name=client_name,
client_version=client_version,
protocol_version=protocol_version,
extra=extra,
)
raise
Expand Down Expand Up @@ -317,6 +329,7 @@ async def handler(req: Any) -> Any:
error="tools/list returned no tools" if empty else None,
client_name=client_name,
client_version=client_version,
protocol_version=protocol_version,
extra=extra,
)

Expand Down Expand Up @@ -368,6 +381,17 @@ def _client_info(server: Any) -> Tuple[Optional[str], Optional[str]]:
return None, None


def _protocol_version(server: Any) -> Optional[str]:
ctx = _request_context(server)
try:
client_params = ctx.session.client_params
if client_params:
return client_params.protocolVersion
except Exception: # noqa: BLE001
pass
return None


def _mcp_session_id(server: Any) -> Optional[str]:
ctx = _request_context(server)
try:
Expand Down
28 changes: 21 additions & 7 deletions posthog/mcp/_instrumentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ async def _maybe_emit_initialize(
client_name: Optional[str],
client_version: Optional[str],
extra: Optional[Dict[str, Any]],
protocol_version: Optional[str] = None,
) -> None:
"""Lazily emit ``$mcp_initialize`` once per session. The Python MCP SDK handles
``InitializeRequest`` inside the session layer (not ``request_handlers``), so we
Expand All @@ -163,6 +164,7 @@ async def _maybe_emit_initialize(
"session_id": session_id,
"client_name": client_name,
"client_version": client_version,
"protocol_version": protocol_version,
"timestamp": datetime.now(timezone.utc),
}
await _apply_event_properties(
Expand All @@ -188,18 +190,21 @@ def resolve_session_and_client(
raw_session_id: Optional[str],
client_name: Optional[str],
client_version: Optional[str],
) -> tuple[Optional[SessionTokenPayload], Optional[str], Optional[str]]:
protocol_version: Optional[str] = None,
) -> tuple[Optional[SessionTokenPayload], Optional[str], Optional[str], Optional[str]]:
"""Decode a replayed ``Mcp-Session-Id`` value as a self-encoded session token,
and backfill the client name/version from it when the live transport supplied
none (the stateless-pod case, where ``initialize`` was never seen here).
and backfill the client name/version/protocol version from it when the live
transport supplied none (the stateless-pod case, where ``initialize`` was never
seen here).

Returns ``(token, client_name, client_version)``; ``token`` is ``None`` when the
header isn't one of our tokens (a plain transport UUID, JWT, or nothing)."""
Returns ``(token, client_name, client_version, protocol_version)``; ``token`` is
``None`` when the header isn't one of our tokens (a plain UUID, JWT, or nothing)."""
token = decode_session_id(raw_session_id)
if token is not None:
client_name = client_name or token.client_name
client_version = client_version or token.client_version
return token, client_name, client_version
protocol_version = protocol_version or token.protocol_version
return token, client_name, client_version, protocol_version


async def prepare_request(
Expand All @@ -211,6 +216,7 @@ async def prepare_request(
request: Dict[str, Any],
extra: Optional[Dict[str, Any]],
token: Optional[SessionTokenPayload] = None,
protocol_version: Optional[str] = None,
) -> str:
"""Resolve the session id, run identify, then lazily emit initialize. Returns
the session id to stamp on the event for this request.
Expand All @@ -228,7 +234,9 @@ async def prepare_request(
identify_event = await handle_identify(data, session_id, request, extra)
if identify_event:
fire_and_forget(capture_event(data, identify_event))
await _maybe_emit_initialize(data, session_id, client_name, client_version, extra)
await _maybe_emit_initialize(
data, session_id, client_name, client_version, extra, protocol_version
)
return session_id


Expand All @@ -243,6 +251,7 @@ async def record_tool_call(
duration_ms: Optional[float] = None,
client_name: Optional[str] = None,
client_version: Optional[str] = None,
protocol_version: Optional[str] = None,
conversation_id: Optional[str] = None,
extra: Optional[Dict[str, Any]] = None,
) -> None:
Expand All @@ -260,6 +269,7 @@ async def record_tool_call(
"duration": duration_ms,
"client_name": client_name,
"client_version": client_version,
"protocol_version": protocol_version,
"conversation_id": conversation_id,
"is_error": False,
}
Expand Down Expand Up @@ -341,6 +351,7 @@ async def record_missing_capability(
arguments: Optional[Dict[str, Any]],
client_name: Optional[str] = None,
client_version: Optional[str] = None,
protocol_version: Optional[str] = None,
extra: Optional[Dict[str, Any]] = None,
) -> None:
"""Record a ``get_more_tools`` call as ``$mcp_missing_capability``, with the
Expand All @@ -354,6 +365,7 @@ async def record_missing_capability(
"parameters": build_captured_mcp_parameters(request),
"client_name": client_name,
"client_version": client_version,
"protocol_version": protocol_version,
}
if isinstance(context, str) and context.strip():
event["user_intent"] = context.strip()
Expand All @@ -376,6 +388,7 @@ async def record_tools_list(
error: Any = None,
client_name: Optional[str] = None,
client_version: Optional[str] = None,
protocol_version: Optional[str] = None,
extra: Optional[Dict[str, Any]] = None,
) -> None:
try:
Expand All @@ -388,6 +401,7 @@ async def record_tools_list(
"duration": duration_ms,
"client_name": client_name,
"client_version": client_version,
"protocol_version": protocol_version,
"is_error": is_error,
"timestamp": datetime.now(timezone.utc),
}
Expand Down
4 changes: 4 additions & 0 deletions posthog/mcp/_posthog_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ def _add_common_properties(event: Event, properties: Dict[str, Any]) -> None:
properties[_P.CLIENT_NAME] = event["client_name"]
if event.get("client_version"):
properties[_P.CLIENT_VERSION] = event["client_version"]
if event.get("protocol_version"):
properties[_P.PROTOCOL_VERSION] = event["protocol_version"]
if event.get("user_intent"):
properties[_P.INTENT] = event["user_intent"]
if event.get("user_intent_source"):
Expand Down Expand Up @@ -183,6 +185,8 @@ def _build_exception_event(event: Event) -> PostHogCaptureEvent:
properties[_P.CLIENT_NAME] = event["client_name"]
if event.get("client_version"):
properties[_P.CLIENT_VERSION] = event["client_version"]
if event.get("protocol_version"):
properties[_P.PROTOCOL_VERSION] = event["protocol_version"]

_add_custom_properties(event, properties)

Expand Down
1 change: 1 addition & 0 deletions posthog/mcp/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class PostHogMCPAnalyticsProperty:

CLIENT_NAME = "$mcp_client_name"
CLIENT_VERSION = "$mcp_client_version"
PROTOCOL_VERSION = "$mcp_protocol_version"
CONVERSATION_ID = "$mcp_conversation_id"
DURATION_MS = "$mcp_duration_ms"
IS_ERROR = "$mcp_is_error"
Expand Down
Loading
Loading