Skip to content
Merged
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
2 changes: 2 additions & 0 deletions contents/docs/mcp-analytics/custom-servers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions contents/docs/mcp-analytics/events.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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()`. |
Expand Down
15 changes: 10 additions & 5 deletions contents/docs/mcp-analytics/installation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand All @@ -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)
Expand Down Expand Up @@ -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).

<CalloutBox icon="IconInfo" title="Python SDK is beta" type="fyi">

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.

</CalloutBox>

Expand Down
36 changes: 29 additions & 7 deletions contents/docs/mcp-analytics/sdk-v2.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
| --- | --- | --- |
Expand All @@ -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.

<CalloutBox icon="IconFlask" title="TypeScript today, Python soon" type="fyi">

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.

</CalloutBox>

## 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()`:
Expand Down Expand Up @@ -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`**:
Expand All @@ -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. |
Expand Down
Loading