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
43 changes: 43 additions & 0 deletions contents/docs/mcp-analytics/custom-servers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,46 @@ await posthog.flush()
// or keep the runtime alive until the flush completes
ctx.waitUntil(posthog.flush())
```

## Python

The Python SDK ships the same custom-dispatcher path as `PostHogMCP`, a subclass of the [`posthog`](/docs/libraries/python) client. Method names are snake_case and arguments are keyword args rather than an options object:

```python
from posthog.mcp import PostHogMCP, get_more_tools_result

posthog = PostHogMCP("phc_your_project_api_key", host="https://us.i.posthog.com")

# Decorate your tools/list response so agents state their intent (and, optionally,
# advertise the get_more_tools virtual tool):
tools = posthog.prepare_tool_list(my_tools, report_missing=True)

# On an inbound tools/call, pull the intent and strip the injected `context`:
prepared = posthog.prepare_tool_call(name, arguments)
if prepared.is_missing_capability:
posthog.capture_missing_capability(context=prepared.intent, distinct_id=user_id)
return get_more_tools_result()

result = run_tool(name, prepared.args)

# Capture the call (fire-and-forget, like posthog.capture):
posthog.capture_tool_call(
name,
intent=prepared.intent,
intent_source=prepared.intent_source,
parameters=arguments,
response=result,
duration_ms=elapsed_ms,
is_error=False,
distinct_id=user_id,
session_id=mcp_session_id,
groups={"organization": org_id},
)

# On the handshake:
posthog.capture_initialize(client_name="claude-code", client_version="1.2.3", distinct_id=user_id)

posthog.flush() # PostHogMCP is a posthog client — flush/shutdown it yourself
```

`PostHogMCP(api_key, missing_capability_tool_name="get_more_tools", **posthog_kwargs)` accepts the standard `posthog` client kwargs (e.g. `host`). 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.
82 changes: 80 additions & 2 deletions contents/docs/mcp-analytics/installation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import CalloutBox from 'components/Docs/CalloutBox'

## Requirements

- Node.js 18 or later
- A TypeScript or JavaScript MCP server built on `@modelcontextprotocol/sdk`. (Running a custom dispatcher with no server object to wrap? See [Custom servers](/docs/mcp-analytics/custom-servers).)
- Node.js 18 or later (TypeScript/JavaScript), or Python 3.10+ — see [Python](#python) below
- An MCP server built on `@modelcontextprotocol/sdk` (TS) or the `mcp` package (Python). (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_…`)

## Install
Expand Down Expand Up @@ -72,6 +72,84 @@ server.tool("search_events", { /* ... */ }, async (args) => {
})
```

## Python

A Python SDK ships inside the [`posthog`](/docs/libraries/python) package (the same way [`posthog.ai`](/docs/ai-engineering) does). Install it with the `mcp` extra:

```bash
pip install posthog[mcp]
```

`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)
- [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)

```python
from posthog import Posthog
from posthog.mcp import instrument
from mcp.server.fastmcp import FastMCP

posthog = Posthog(
"phc_your_project_api_key",
host="https://us.i.posthog.com", # or https://eu.i.posthog.com
)
server = FastMCP("my-server")

# register your tools as usual...

analytics = instrument(server, posthog)
```

Options are passed as `MCPAnalyticsOptions`, the snake_case equivalent of the TypeScript options:

```python
from posthog.mcp import instrument
from posthog.mcp.types import MCPAnalyticsOptions, UserIdentity

instrument(server, posthog, MCPAnalyticsOptions(
context=True, # inject the `context` intent argument (default)
report_missing=True, # register the get_more_tools virtual tool
enable_conversation_id=True, # stitch calls across reconnects
identify=lambda request, extra: UserIdentity(distinct_id="user_123"),
))
```

`MCPAnalyticsOptions` fields (the TypeScript [Configuration](#configuration) table below uses camelCase — these are the Python names):

| Option | Type | Default | What it does |
|---|---|---|---|
| `context` | `bool \| MCPAnalyticsContextOptions` | `True` | Inject the `context` intent argument into every tool. |
| `report_missing` | `bool` | `False` | Register the `get_more_tools` virtual tool. |
| `missing_capability_tool_name` | `str` | `"get_more_tools"` | Rename the virtual tool registered by `report_missing`. |
| `enable_conversation_id` | `bool` | `False` | Inject an optional `conversation_id` argument to stitch calls. |
| `enable_exception_autocapture` | `bool` | `True` | Emit a `$exception` sibling on failed tool calls. |
| `identify` | `(request, extra) -> UserIdentity \| None` (sync or async) | — | Map a request to one of your users. |
| `intent_fallback` | `(request, extra) -> str \| None` | — | Provide intent when the agent didn't pass `context`. |
| `before_send` | `(event) -> event \| None` | — | Inspect/modify/drop each event before send. |
| `event_properties` | `(request, extra) -> dict` | — | Properties merged onto every event. |
| `logger` | `(message: str) -> None` | no-op | STDIO-safe log sink. |

### Flushing on exit

The `posthog` client batches events asynchronously and you own its lifecycle. On the `instrument()` path, auto-captured events are scheduled in the background — `await analytics.flush()` waits for in-flight events, then `posthog.flush()` / `posthog.shutdown()` sends them. Call this from your shutdown/`SIGTERM` handler so trailing events aren't dropped (see [`examples/mcp_analytics_demo.py`](https://github.com/PostHog/posthog-python/blob/main/examples/mcp_analytics_demo.py) for a runnable end-to-end example):

```python
analytics = instrument(server, posthog)
# ... serve ...
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 with `capture_tool_call()`, `capture_initialize()`, `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 alpha" type="fyi">

The Python SDK is alpha and TypeScript-only features may land first. It emits the identical `$mcp_*` events documented on the [events](/docs/mcp-analytics/events) page.

</CalloutBox>

## Configuration

The `posthog` client is passed as the required second positional argument — not in this options object. `instrument()` accepts these options as an optional third argument:
Expand Down
Loading