diff --git a/contents/docs/mcp-analytics/custom-servers.mdx b/contents/docs/mcp-analytics/custom-servers.mdx index 6824fda94b2a..8b8b52c0674c 100644 --- a/contents/docs/mcp-analytics/custom-servers.mdx +++ b/contents/docs/mcp-analytics/custom-servers.mdx @@ -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. diff --git a/contents/docs/mcp-analytics/installation.mdx b/contents/docs/mcp-analytics/installation.mdx index 13b1799c856f..a067a1d7b2c1 100644 --- a/contents/docs/mcp-analytics/installation.mdx +++ b/contents/docs/mcp-analytics/installation.mdx @@ -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 @@ -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). + + + +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. + + + ## 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: