diff --git a/contents/docs/mcp-analytics/custom-servers.mdx b/contents/docs/mcp-analytics/custom-servers.mdx index eb3b5dfd0af4..436b3d844541 100644 --- a/contents/docs/mcp-analytics/custom-servers.mdx +++ b/contents/docs/mcp-analytics/custom-servers.mdx @@ -148,6 +148,8 @@ posthog.flush() # PostHogMCP is a posthog client — flush/shutdown it yourself `PostHogMCP(api_key, missing_capability_tool_name="get_more_tools", mcp_exception_autocapture=True, **posthog_kwargs)` accepts the standard `posthog` client kwargs (e.g. `host`). Set `mcp_exception_autocapture=False` to stop a failed tool call from emitting a `$exception` sibling. As in TypeScript, the wrapping-path hooks (`identify`, `context`, `intent_fallback`, `event_properties`) don't apply here — pass identity and properties on each `capture_*` call. +The `capture_*` methods also take `error_type` (a low-cardinality failure label for `$mcp_error_type`), `client_user_agent`, and `vendor_client` — the same fields documented in the [event reference](/docs/mcp-analytics/events#core-properties). + ### Stateless / multi-pod dispatchers On a stateless deployment (a fresh server per request, often across pods) there's no connection to carry a session, so `$session_id` fragments and the client name/version — sent only at `initialize` — go missing from later requests. Add the mint middleware to your ASGI app once. It mints a self-encoded token onto the `Mcp-Session-Id` response header at `initialize` and decodes the client's replay on every later request, so every pod recovers the same values with no shared store: diff --git a/contents/docs/mcp-analytics/events.mdx b/contents/docs/mcp-analytics/events.mdx index f16fb72d3390..717ff8522475 100644 --- a/contents/docs/mcp-analytics/events.mdx +++ b/contents/docs/mcp-analytics/events.mdx @@ -37,8 +37,8 @@ Present on most `mcp_*` events. | `$mcp_listed_tool_names` | string[] | Names of tools advertised in a `tools/list` response. Only on `$mcp_tools_list`. Useful for joining against `$mcp_tool_call` via `$session_id` to find tools advertised but never called. | | `$mcp_duration_ms` | number (ms) | Wall-clock duration of the tool call | | `$mcp_is_error` | boolean | True if the tool threw or returned `isError: true` | -| `$mcp_error_type` | string | Low-cardinality failure category, so you can break errors down by cause without joining to the `$exception` sibling. Defaults to the thrown error's type; pass an explicit label to categorize failures yourself (e.g. `validation`, `permission`, `timeout`, `rate_limited`). Only set when `$mcp_is_error` is true, and TypeScript only for now — the [Python SDK](/docs/mcp-analytics/installation#python) doesn't emit it yet. | -| `$mcp_error_message` | string | The failed call's error message, truncated and passed through the same redaction as `$mcp_parameters` and `$mcp_response`. Only set when `$mcp_is_error` is true, and TypeScript only for now. | +| `$mcp_error_type` | string | Low-cardinality failure category, so you can break errors down by cause without joining to the `$exception` sibling. Defaults to the thrown error's type; pass an explicit label to categorize failures yourself (e.g. `validation`, `permission`, `timeout`, `rate_limited`). Only set when `$mcp_is_error` is true. | +| `$mcp_error_message` | string | The failed call's error message, truncated and passed through the same redaction as `$mcp_parameters` and `$mcp_response`. Only set when `$mcp_is_error` is true. | | `$mcp_server_name` | string | `server._serverInfo.name` | | `$mcp_server_version` | string | `server._serverInfo.version` | | `$mcp_client_name` | string | The calling client as it reports itself. Resolved per request, field by field, through the MCP SDK v2 request envelope, then `params._meta`, then the server's own `getClientVersion()`. | diff --git a/contents/docs/mcp-analytics/installation.mdx b/contents/docs/mcp-analytics/installation.mdx index c6ab83858687..c5f385db3a28 100644 --- a/contents/docs/mcp-analytics/installation.mdx +++ b/contents/docs/mcp-analytics/installation.mdx @@ -14,7 +14,7 @@ import WizardCommand from 'components/WizardCommand' ## Requirements - Node.js 20.20+ or 22.22+ (TypeScript/JavaScript), or Python 3.10+ — see [Python](#python) below -- An MCP server built on the MCP TypeScript SDK — either major, `@modelcontextprotocol/sdk` (v1) or `@modelcontextprotocol/{core,server,client}` (v2), see [MCP SDK v2](/docs/mcp-analytics/sdk-v2) — or the `mcp` package (Python). (Running a custom dispatcher with no server object to wrap? See [Custom servers](/docs/mcp-analytics/custom-servers).) +- An MCP server built on the MCP TypeScript SDK — either major, `@modelcontextprotocol/sdk` (v1) or `@modelcontextprotocol/{core,server,client}` (v2) — or the Python MCP SDK, also either major (`mcp>=1.26,<3`). See [MCP SDK v2](/docs/mcp-analytics/sdk-v2). (Running a custom dispatcher with no server object to wrap? See [Custom servers](/docs/mcp-analytics/custom-servers).) - A PostHog [project API key](/docs/getting-started/project-token) (`phc_…`) ## AI wizard @@ -257,11 +257,12 @@ A Python SDK ships inside the [`posthog`](/docs/libraries/python) package (the s pip install posthog ``` -`instrument()` needs the MCP SDK at runtime, but you already have it — you built your server with `mcp` or `fastmcp`, so it's treated as a peer dependency rather than bundled. (`PostHogMCP` for custom dispatchers needs nothing beyond `posthog`.) +`instrument()` needs the MCP SDK at runtime, but you already have it — you built your server with `mcp` or `fastmcp`, so it's treated as a peer dependency rather than bundled. Both `mcp` majors are supported (`mcp>=1.26,<3`), detected at runtime. (`PostHogMCP` for custom dispatchers needs nothing beyond `posthog`.) `instrument(server, posthog_client, options?)` works with every common Python MCP server: -- `FastMCP` and the low-level `Server` from the official [`modelcontextprotocol/python-sdk`](https://github.com/modelcontextprotocol/python-sdk) (the `mcp` package) +- `FastMCP` and the low-level `Server` from the official [`modelcontextprotocol/python-sdk`](https://github.com/modelcontextprotocol/python-sdk) (the `mcp` package, 1.x) +- `MCPServer` — FastMCP's new name on `mcp` 2.x — and the v2 low-level `Server`, see [MCP SDK v2](/docs/mcp-analytics/sdk-v2#python) - [jlowin's standalone **FastMCP 2.0**](https://github.com/jlowin/fastmcp) (the separate `fastmcp` package) - `PostHogMCP` for custom dispatchers with no server object (see below) @@ -276,6 +277,10 @@ posthog = Posthog( ) server = FastMCP("my-server") +# On MCP SDK 2.x, FastMCP was renamed — instrument() works the same: +# from mcp.server.mcpserver import MCPServer +# server = MCPServer("my-server") + # register your tools as usual... analytics = instrument(server, posthog) @@ -341,13 +346,13 @@ await analytics.flush() # drain in-flight auto-capture events posthog.shutdown() # flush + stop the posthog client ``` -No server object to wrap (a custom HTTP/edge dispatcher)? Use `PostHogMCP`, a `posthog` client subclass (needs nothing beyond `posthog` — no MCP SDK) with `capture_tool_call()`, `capture_initialize()`, `prepare_tool_list()`, and `prepare_tool_call()` — the Python equivalent of [Custom servers](/docs/mcp-analytics/custom-servers). +No server object to wrap (a custom HTTP/edge dispatcher)? Use `PostHogMCP`, a `posthog` client subclass (needs nothing beyond `posthog` — no MCP SDK) with `capture_tool_call()`, `capture_initialize()`, `capture_tools_list()`, `capture_missing_capability()`, `prepare_tool_list()`, and `prepare_tool_call()` — the Python equivalent of [Custom servers](/docs/mcp-analytics/custom-servers). The Python SDK is in beta (pre-1.0); the API may still change before `v1`, and some TypeScript-only features may land first. It emits the identical `$mcp_*` events documented on the [events](/docs/mcp-analytics/events) page. -It doesn't support the `2026-07-28` protocol revision yet — that's in progress. See [MCP SDK v2](/docs/mcp-analytics/sdk-v2) for what that revision changes. +It supports both `mcp` majors and the `2026-07-28` protocol revision. See [MCP SDK v2](/docs/mcp-analytics/sdk-v2) for what that revision changes. diff --git a/contents/docs/mcp-analytics/sdk-v2.mdx b/contents/docs/mcp-analytics/sdk-v2.mdx index 0ca11ceba8d2..e5a49286449e 100644 --- a/contents/docs/mcp-analytics/sdk-v2.mdx +++ b/contents/docs/mcp-analytics/sdk-v2.mdx @@ -4,7 +4,7 @@ title: MCP SDK v2 import CalloutBox from 'components/Docs/CalloutBox' -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 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](#python) below. | Your imports | Major | Protocol revisions it serves | | --- | --- | --- | @@ -13,12 +13,6 @@ The MCP TypeScript SDK has two majors. `@posthog/mcp` supports both, and detects Use `@posthog/mcp` 0.11.2 or later on v2. Earlier versions captured nothing, silently. - - -This page covers the MCP **TypeScript** SDK. The [Python SDK](/docs/mcp-analytics/installation#python) is still in active development: it emits the same `$mcp_*` events, but doesn't support the `2026-07-28` revision yet. That support is on the way. - - - ## 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()`: @@ -59,6 +53,32 @@ instrument(server, posthog, { }) ``` +## Python + +The [Python SDK](/docs/mcp-analytics/installation#python) supports both `mcp` majors too (`mcp>=1.26,<3`), detected at runtime. On 2.x, `FastMCP` was renamed — same `instrument()` call: + +```python +from mcp.server.mcpserver import MCPServer +from posthog.mcp import instrument + +server = MCPServer("my-server") +instrument(server, posthog) +``` + +The low-level `Server` works on both majors. jlowin's standalone `fastmcp` package pins `mcp<2`, so it stays on the 1.x path — nothing to change there. + +The header gotcha above applies in Python too: the request context reaches your 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: + +```python +from posthog.mcp import get_request_headers + +def identify(request, extra): + headers = get_request_headers(extra) or {} + return resolve_user(headers.get("authorization")) +``` + +[Sessions on `2026-07-28`](#sessions-on-2026-07-28) work as described below: `enable_conversation_id=True` is the only shared `$session_id` on that revision, and both SDKs derive the same session id from the same `conversation_id`, so a mixed TypeScript-and-Python fleet agrees on sessions. + ## Sessions on `2026-07-28` That revision removed the `initialize` handshake and the `Mcp-Session-Id` header, so the [stateless session token](/docs/mcp-analytics/installation#stateless-and-multi-pod-servers) doesn't apply to it — and left alone, **every request becomes its own `$session_id`**: @@ -76,6 +96,8 @@ On that revision the client sends its name and version only at `initialize`. If ## 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. |