MCP SDK v2
Contents
The MCP TypeScript SDK has two majors. @posthog/mcp supports both and detects which one you're on at runtime — neither is a dependency of the package. The Python MCP SDK made the same split; see Python below.
| Your imports | Major | Protocol revisions it serves |
|---|---|---|
@modelcontextprotocol/sdk | v1 | 2025-11-25 and earlier |
@modelcontextprotocol/core, /server, /client | v2 | 2025-11-25 and 2026-07-28 |
Use @posthog/mcp 0.11.2 or later on v2. Earlier versions captured nothing, silently.
Setup
Same call as v1. The only difference is where McpServer comes from, and that v2 registers tools with registerTool() instead of the removed server.tool():
The low-level Server works the same way. If you previously called instrument(server.server) to get past the old compatibility check, you can go back to instrument(server).
If your callbacks read headers, change them
v1 puts headers at extra.requestInfo.headers. v2 puts the request at extra.http.req, a WHATWG Request whose headers only answer to .get(). A v1-shaped read returns undefined on v2 — so identify() returns null and every event goes out anonymous, with no error anywhere.
Use the exported helper in identify, intentFallback, eventProperties and beforeSend. It handles both majors and returns a plain lowercase-keyed object:
Python
The Python SDK supports both official mcp majors (mcp>=1.26,<3) and detects them at runtime. MCP SDK 2.x support requires posthog 7.40.0 or later. On 2.x, FastMCP was renamed — the instrument() call stays the same:
The low-level Server works on both majors. jlowin's standalone fastmcp package pins mcp<2, so it stays on the 1.x adapter path.
The header gotcha above applies in Python too: request context reaches callbacks in a different shape on each major. Use the exported helper in identify, intent_fallback, and event_properties. It returns a lowercase-keyed dict on HTTP transports, None on stdio, and never raises:
Sessions on 2026-07-28 work as described below. enable_conversation_id=True provides a shared $session_id on that revision, and both PostHog SDKs derive the same session id from the same conversation handle.
Sessions on 2026-07-28
That revision removed the initialize handshake and the Mcp-Session-Id header, so the stateless session token doesn't apply to it — and left alone, every request becomes its own $session_id:
enableConversationId: true— the only way to get a shared$session_idon this revision. The SDK injects aconversation_idparameter, mints one when the agent doesn't send it, and derives$session_idfrom it, so a conversation's calls land in one session. Off by default; turn it on if you want sessions.identify— attributes calls to a person viadistinct_id. Worth wiring up either way, but note it groups by user, not by session, and doesn't give you a$session_id.
Note that revision is a property of each request, not of your server: a v2 server serves 2025-11-25 traffic too, and most clients still negotiate it.
On that revision the client sends its name and version only at initialize. If your server builds a fresh instance per request, the SDK bridges this with a session token — but the token only reaches the client if the transport writes response headers after your handler runs. @rekog/mcp-nest with enableJsonResponse: true does; createMcpHandler's legacy path doesn't, so expect $mcp_client_name and $mcp_client_version to be absent there. $mcp_protocol_version still arrives.
Capture model identity on both revisions
The MCP wire doesn't include the model behind a client on either revision. On TypeScript servers, captureModel: true injects a required llm_model tool argument so the agent can report it. The SDK strips the argument before your handler runs and captures $mcp_llm_model_source = "self_reported" with the answer.
This works on 2025-11-25 and 2026-07-28. The value is unverified, so use it to compare model-level quality and errors, not for billing or security. Model capture hasn't shipped in Python yet.
MCP Apps
@posthog/mcp preserves MCP App tool metadata, ui:// resources, structured tool output, result metadata, HTML, and content security policy metadata on both supported revisions. On the tested high-level McpServer path, injected context and llm_model arguments don't reach the App handler.
The App's tool call is captured with its intent, self-reported model, and protocol version. Automatic analytics for the App's resources/list and resources/read requests on the tested high-level server path haven't shipped yet. The resource payloads still pass through unchanged.
Not instrumented yet
These gaps apply to the TypeScript and Python SDKs alike.
2026-07-28 feature | What you get today |
|---|---|
Tasks (io.modelcontextprotocol/tasks) | A tool returning a task handle records an instant success, so task-based tools look fast and always-succeeding. |
Multi round-trip (resultType: "input_required") | Each round counts as its own $mcp_tool_call, inflating call counts and durations. |
server/discover | Not captured — no session-start event on this revision. |
| MCP App resource requests on the high-level server path | App resources pass through unchanged, but resources/list and resources/read don't emit analytics yet. |
Mcp-Method / Mcp-Name headers | Not read. |
clientCapabilities in _meta | Not captured. clientInfo and protocol version are. |
The first two make numbers wrong rather than missing, so check them before trusting a dashboard for task-based or multi-round-trip tools.