Skip to content

PostHog

PostHog connects an agent to PostHog’s hosted MCP server so it can query product analytics, run SQL, and manage feature flags, experiments, dashboards, surveys, and error tracking, including tools that make changes. The key you connect with decides what those tools can reach.

Source

While Pydantic AI Harness is on 0.x releases, the API may change between minor releases; when it does, deprecation warnings and release-note migration guidance tell you (or your agent) exactly how to upgrade. See the version policy.

Inside your agent, the PostHog capability connects to PostHog's hosted MCP server at mcp.posthog.com/mcp with a personal API key as a bearer token. By default the server offers one command-driven tool, which the model sees as posthog_posthog. The features option is sent as a ?features= query that limits the server to those feature groups, and read_only=True sends the x-posthog-read-only header so the server drops every tool that makes changes.

Before you start

Create a personal API key with the MCP Server preset (US or EU Cloud). US and EU Cloud accounts use the same server. See the provider setup.

Some PostHog tools use an LLM on PostHog’s side. Those need AI data processing enabled for your organization and may be billed as PostHog AI usage.

Installation

Terminal
pip install "pydantic-ai-harness[posthog]" "pydantic-ai-slim[openai]"

The second package installs the OpenAI provider the example uses. For another model, install that provider’s extra instead.

Connect

from pydantic_ai import Agent
from pydantic_ai_harness import PostHog

agent = Agent('openai:gpt-6.1-sol', capabilities=[PostHog()])
result = agent.run_sync('Which feature flags are active in my project?')
print(result.output)

Set POSTHOG_PERSONAL_API_KEY to a PostHog personal API key, or pass auth= a key. On your own machine, auth='oauth' signs you in through the browser instead. To serve several users from one agent, pass a function instead (see Per-user credentials).

Per-user credentials

auth decides which PostHog account each run uses:

authAccount used
Not set, None, or ''POSTHOG_PERSONAL_API_KEY. If that is not set either, creating the agent raises an error.
A personal API key or OAuth access tokenThat key, for every run.
'oauth'The account you sign in to through the browser. This only works on your own machine.
A functionCalled at the start of each run, and can be async. The key it returns is used for that run. If it returns None or '', that run has no PostHog tools. A function never uses POSTHOG_PERSONAL_API_KEY, and must not return 'oauth'.

A fixed key or POSTHOG_PERSONAL_API_KEY suits a script or an agent on your own machine, where every run is the same account.

In an app where each user connects their own PostHog account, one agent serves all of them, so the key cannot be fixed when the agent is created. Pass a function that reads the current user’s key from the run’s deps:

from dataclasses import dataclass

from pydantic_ai import Agent, RunContext
from pydantic_ai_harness import PostHog


@dataclass
class Deps:
    posthog_key: str | None


def posthog_key(ctx: RunContext[Deps]) -> str | None:
    return ctx.deps.posthog_key


agent = Agent('openai:gpt-6.1-sol', deps_type=Deps, capabilities=[PostHog(auth=posthog_key)])

Each run connects as its own user, so concurrent runs never share an account. read_only and features still apply to every run.

Your app gets each user’s key, stores it, and refreshes it if it is an OAuth token. For example, a settings page where each user pastes their own personal API key, or a “Connect PostHog” button that signs them in with PostHog OAuth and saves the access token to their account. Before each run, put it in the deps, or load it in an async auth function.

With durable execution such as Temporal, read the key from the run’s deps rather than from a global, since the function may run in another process. The capability’s id defaults to posthog, so defer_loading=True works without one. To add more than one PostHog to an agent, give each a distinct id and tool_prefix; two that share an id but differ raise an error.

Provider settings

By default the server offers everything through a single posthog tool that the agent drives with commands, so narrow what it can reach on the server:

  • features=['flags', 'insights', 'sql'] limits the server to those feature groups. Leave it unset for every group.
  • read_only=True asks the server for its read-only mode, which drops every tool that makes changes.

The key’s scopes, and the organizations and projects it can reach, still decide what the agent can access.

Tool names

The agent sees each of the server’s tools under the posthog_ prefix, as in posthog_<tool>, so they don’t collide with other tools on the agent. The command-driven posthog tool the server offers by default is posthog_posthog. Set tool_prefix to use a different prefix, or tool_prefix=None to keep the names the server gives them.

read_only selects tools by what the server marks as read-only, not by name, so the prefix does not change what it keeps. features names PostHog feature groups, which the server selects before any prefix applies. A toolset wrapper on capability.get_toolset(), such as filtered(), sees the names the agent sees, prefix included.

Upgrading

PostHog used to give the agent the server’s tool names unprefixed. They now reach the agent as posthog_<tool>, so the command-driven posthog tool the server offers by default is now posthog_posthog. Code that refers to them by name, such as a filtered() wrapper, an approval rule, or a ToolGuardrail(tools=[...]), needs the new names. A rule that no longer matches stops applying, so an approval rule or guardrail that named an old tool lets its calls through without approval or checks until you update it. Pass tool_prefix=None to keep the old names. If you wrapped PostHog in PrefixTools to avoid collisions, you can drop the wrapper and set tool_prefix instead; keeping it adds both prefixes.

  • Durable runs in flight. A Temporal or DBOS run replays the model responses it recorded, and their tool calls use the old names, so a run started before the change that calls a PostHog tool after it no longer replays. Activity and step names come from the capability’s id, which is unchanged. Let those runs finish, keep tool_prefix=None until they have, or deploy the change with Temporal worker versioning or a new DBOS application version.

Tool selection and approval

To filter tools or require approval in your application, wrap the toolset with the existing toolset wrappers. For example, this asks for approval before every tool call:

from pydantic_ai import Agent
from pydantic_ai.messages import DeferredToolRequests
from pydantic_ai_harness import PostHog

capability = PostHog()
agent = Agent(
    'openai:gpt-6.1-sol',
    toolsets=[capability.get_toolset().approval_required()],
    output_type=[str, DeferredToolRequests],
)

Handle the approval requests with the deferred tools workflow. To cap the size of tool output, add Tool Output Limits.

Connection customization

Pass client when you need control of the connection itself: your own FastMCP client or transport, for example one pointing at a PostHog MCP server you run for a self-hosted instance, a proxy, or MCP handlers. The client then owns the URL and authentication, so passing client together with auth or features raises an error. With a client, read_only=True keeps only the tools the server marks as read-only, instead of asking the server for read-only mode. PostHog’s default mode serves one posthog tool that is not marked read-only, so this filter leaves no PostHog tools and emits an MCPReadOnlyNoToolsWarning; have your client send the x-posthog-read-only: true header instead. include_instructions=False stops the server’s own instructions from reaching the agent.

A client is one connection shared by every run; see Per-user credentials to connect each user separately.

Telemetry

PostHog emits no spans of its own. Core’s instrumentation already records each PostHog tool call as a tool span.

Define the agent in YAML or JSON

Loading a YAML file also needs the spec extra:

Terminal
pip install "pydantic-ai-slim[spec]"
# agent.yaml
model: openai:gpt-6.1-sol
capabilities:
  - PostHog:
      features: [flags, insights]
      read_only: true
from pydantic_ai import Agent
from pydantic_ai_harness import PostHog

agent = Agent.from_file('agent.yaml', custom_capability_types=[PostHog])

API reference

PostHog

Bases: AbstractCapability[AgentDepsT]

Let an agent query PostHog product analytics and manage its resources.

Set POSTHOG_PERSONAL_API_KEY or pass a PostHog personal API key as auth. The agent can then do whatever the key’s scopes allow.

from pydantic_ai import Agent
from pydantic_ai_harness import PostHog

agent = Agent('openai:gpt-5.6-sol', capabilities=[PostHog()])

Attributes

id

Stable capability and toolset ID, so defer_loading=True needs none.

One PostHog is one connection to one account. Two sharing this id are one connection stated twice when they agree, and an error when they differ; give each its own id to keep both.

Type: str | None Default: _ID

description

Routing description used when the capability is loaded on demand.

Type: str | None Default: _DEFAULT_DESCRIPTION

tool_prefix

Prepended with _ to the server’s tool names, as in posthog_<tool>, so they don’t collide with other tools.

Give two instances distinct prefixes to use both. None keeps the names the server gives its tools.

Type: str | None Default: 'posthog'

auth

A PostHog personal API key or OAuth access token, 'oauth' to sign in through the browser locally, or a sync or async function of the run context that returns one.

Unset, it uses POSTHOG_PERSONAL_API_KEY. A function never does: if it returns None or '', that run has no PostHog tools.

Type: str | Callable[[RunContext[AgentDepsT]], str | None | Awaitable[str | None]] | None Default: field(default=None, repr=False)

read_only

Ask the server for read-only tools. With a custom client, keep only the tools the server marks as read-only.

PostHog’s default mode serves one posthog tool that is not marked read-only, so with a custom client this leaves no PostHog tools; send the x-posthog-read-only: true header from the client instead.

Type: bool Default: False

features

PostHog feature groups to offer, such as 'flags' or ['flags', 'insights']. None offers every group.

Type: str | Sequence[str] | None Default: None

include_instructions

Pass the server’s own instructions to the agent.

Type: bool Default: True

client

Your own MCP client or transport, for full control of the connection.

It cannot be combined with auth or features.

Type: MCPToolsetClient | None Default: field(default=None, repr=False)

Methods

combine

@classmethod

def combine(
    cls,
    capabilities: Sequence[AbstractCapability[AgentDepsT]],
) -> AbstractCapability[AgentDepsT]

Two under one id are one connection stated twice; two that disagree raise rather than merge.

Returns

AbstractCapability[AgentDepsT]

get_toolset
def get_toolset() -> AbstractToolset[AgentDepsT]

Return the PostHog MCP tools.

Returns

AbstractToolset[AgentDepsT]