# PostHog MCP The official MCP server for PostHog. PostHog makes your product self-driving — it reads your data and ships changes with you, never without you — and this server gives MCP clients (Claude, Cursor, VS Code, Zed, and more) that full surface: analytics and SQL, dashboards, experiments, feature flags, surveys, session replay, error tracking, and more. Documentation: https://posthog.com/docs/model-context-protocol ## Use the MCP Server ### Quick install You can install the MCP server automatically into Cursor, Claude, Claude Code, VS Code and Zed by running the following command: ```bash npx @posthog/wizard@latest mcp add ``` ### Manual install 1. Obtain a personal API key using the [MCP Server preset](https://us.posthog.com/settings/user-api-keys?preset=mcp_server). 2. Add the MCP configuration to your desktop client (e.g. Cursor, Windsurf, Claude Desktop) and add your personal API key ```json { "mcpServers": { "posthog": { "command": "npx", "args": [ "-y", "mcp-remote@latest", "https://mcp.posthog.com/mcp", "--header", "Authorization:${POSTHOG_AUTH_HEADER}" ], "env": { "POSTHOG_AUTH_HEADER": "Bearer {INSERT_YOUR_PERSONAL_API_KEY_HERE}" } } } } ``` ### Minimal Node client (Streamable HTTP) If you want to call MCP from Node (outside an IDE), use the Model Context Protocol SDK’s **Streamable HTTP** transport. - **Auth:** Use a **personal** PostHog API key and pass it as a Bearer token in `Authorization`. - **Accept header:** Clients **must** include `Accept: application/json, text/event-stream`. - **Lifecycle:** MCP requires `initialize` then a client `notifications/initialized`; the SDK performs this during `connect()`. ```js // tools-list.mjs import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' import { ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js' import { mkdirSync, writeFileSync } from 'node:fs' import { join } from 'node:path' import { URL } from 'node:url' const AUTH = process.env.POSTHOG_AUTH_HEADER // "Bearer phx_…" const MCP_URL = process.env.MCP_URL || 'https://mcp.posthog.com/mcp' if (!AUTH?.startsWith('Bearer ')) { console.error('Set POSTHOG_AUTH_HEADER="Bearer phx_..."') process.exit(1) } const transport = new StreamableHTTPClientTransport(new URL(MCP_URL), { requestInit: { headers: { Authorization: AUTH, // Required for Streamable HTTP (JSON + SSE) Accept: 'application/json, text/event-stream', }, }, serverInfo: { name: 'example-node-client', version: '0.0.1' }, }) const client = new Client({ name: 'example-node-client', version: '0.0.1' }) // Handles initialize + notifications/initialized await client.connect(transport) const toolsResp = await client.request({ method: 'tools/list' }, ListToolsResultSchema) // { tools: [...] } const tools = toolsResp?.tools ?? [] console.log('Tools:', tools.length) // (Optional) Save the full JSON-RPC envelope to a file (run from repo root) const envelope = { jsonrpc: '2.0', id: 'list-1', result: toolsResp } mkdirSync('reports', { recursive: true }) writeFileSync(join('reports', 'tools-list-http.json'), JSON.stringify(envelope, null, 2)) console.log('Saved: reports/tools-list-http.json') await client.close() ``` **Why these headers & steps?** - Streamable HTTP requires the `Accept` header to include **both** JSON and SSE. - After `initialize`, the client must send `notifications/initialized`; the SDK does this for you in `connect()`. See also the main PostHog MCP docs for available tools and setup flows: [https://posthog.com/docs/model-context-protocol](https://posthog.com/docs/model-context-protocol) ### Example Prompts Below are detailed examples showing realistic prompts and expected outputs: #### Example 1: Feature flag management **Prompt:** "Create a feature flag called 'new-checkout-flow' that's enabled for 20% of users, and show me the configuration" **What happens:** 1. The `create-feature-flag` tool creates the flag with a 20% rollout 2. Returns the flag configuration including the key, rollout percentage, and targeting rules **Expected output:** ```text Created feature flag 'new-checkout-flow': - Key: new-checkout-flow - Active: true - Rollout: 20% of all users - URL: https://us.posthog.com/project//feature_flags/12345 ``` #### Example 2: Analytics query **Prompt:** "How many unique users signed up in the last 7 days, broken down by day?" **What happens:** 1. The `query-trends` tool executes a trends query filtering for `$signup` events 2. Returns daily counts with unique user aggregation **Expected output:** ```text Signups over the last 7 days: | Date | Unique users | |------------|--------------| | 2025-01-17 | 142 | | 2025-01-18 | 156 | | 2025-01-19 | 98 | | ... | ... | Total: 847 unique signups ``` #### Example 3: A/B test creation and monitoring **Prompt:** "Create an A/B test for our pricing page that measures conversion to the checkout page" **What happens:** 1. The `experiment-create` tool creates an experiment with control/test variants 2. Sets up a funnel metric: pricing page view → checkout page view 3. Creates an associated feature flag for variant assignment **Expected output:** ```text Created experiment 'Pricing page test': - Feature flag: pricing-page-test - Variants: control (50%), test (50%) - Primary metric: Funnel conversion (pricing_page → checkout) - Status: Draft (ready to launch) - URL: https://us.posthog.com/project//experiments/789 ``` #### Example 4: Error investigation **Prompt:** "What are the top 5 errors in my project this week and how many users are affected?" **What happens:** 1. The `query-error-tracking-issues-list` tool fetches error groups sorted by occurrence count 2. Returns error details including affected user counts **Expected output:** ```text Top 5 errors this week: 1. TypeError: Cannot read property 'id' of undefined - Occurrences: 1,247 - Users affected: 89 - First seen: 2 days ago 2. NetworkError: Failed to fetch - Occurrences: 856 - Users affected: 234 - First seen: 5 days ago ... ``` #### Quick prompts For simpler queries, you can use shorter prompts: - "What feature flags do I have active?" - "Show me my LLM costs this week" - "List my dashboards" - "What events are being tracked?" ### Feature Filtering You can limit which tools are available by adding query parameters to the MCP URL. If no features are specified, all tools are available. When features are specified, only tools matching those features are exposed. ```text https://mcp.posthog.com/mcp?features=flags,workspace,dashboards ``` Available features: | Feature | Description | | ------------------------ | ----------------------------------------------------------------------------------------------- | | `actions` | [Actions](https://posthog.com/docs/data/actions) | | `alerts` | [Alerts](https://posthog.com/docs/alerts) | | `annotations` | [Annotations](https://posthog.com/docs/product-analytics/annotations) | | `batch_exports` | Data pipelines | | `business_knowledge` | Business knowledge | | `canvas` | Canvas | | `cohorts` | [Cohorts](https://posthog.com/docs/data/cohorts) | | `conversations` | Conversations | | `core` | Core utilities (project switching, docs search) | | `customer_analytics` | [Customer analytics](https://posthog.com/docs/customer-analytics) | | `dashboards` | [Dashboards](https://posthog.com/docs/product-analytics/dashboards) | | `data_catalog` | Data catalog | | `data_schema` | Data schema exploration | | `data_warehouse` | [Data warehouse](https://posthog.com/docs/data-warehouse) | | `debug` | Debug and diagnostic tools | | `docs` | PostHog documentation search | | `early_access_features` | [Early access features](https://posthog.com/docs/feature-flags/early-access-feature-management) | | `endpoints` | [Endpoints](https://posthog.com/docs/endpoints) | | `engineering_analytics` | Engineering analytics | | `error_tracking` | [Error tracking alerts](https://posthog.com/docs/error-tracking) | | `events` | Event and property definitions | | `experiments` | [Experiments](https://posthog.com/docs/experiments) | | `feedback` | Send feedback to the PostHog team | | `field_notes` | Field notes | | `flags` | [Feature flags](https://posthog.com/docs/feature-flags) | | `health_issues` | Health | | `hog_function_templates` | CDP function template browsing | | `hog_functions` | [Functions](https://posthog.com/docs/cdp) | | `insights` | [Insights & analytics](https://posthog.com/docs/product-analytics/insights) | | `integrations` | Integrations | | `links` | PostHog app URL generation | | `llm_analytics` | [AI observability](https://posthog.com/docs/ai-observability) | | `logs` | [Logs](https://posthog.com/docs/logs) | | `managed_migrations` | Managed migrations | | `marketing_analytics` | Marketing analytics | | `messaging` | Messaging | | `mcp_analytics` | MCP analytics | | `mcp_store` | MCP Store | | `metrics` | Metrics | | `notebooks` | [Notebooks](https://posthog.com/docs/notebooks) | | `persons` | [Persons](https://posthog.com/docs/data/persons) | | `platform_features` | Platform Features | | `product_analytics` | Product analytics | | `reminders` | Reminders | | `replay` | [Session replays](https://posthog.com/docs/session-replay) | | `replay_vision` | Replay vision | | `reverse_proxy` | Reverse proxy record management | | `review_hog` | ReviewHog | | `signals` | [Signals](https://posthog.com/docs/self-driving) | | `skills` | Skills | | `sql` | SQL query execution | | `stamphog` | Stamphog | | `streamlit_apps` | Streamlit apps | | `subscriptions` | Subscriptions | | `surveys` | [Surveys](https://posthog.com/docs/surveys) | | `tasks` | [Tasks](https://posthog.com/docs/posthog-desktop/tasks) | | `tracing` | Tracing | | `user_interviews` | User interview topics | | `visual_review` | Visual review | | `warehouse_sources` | Warehouse sources | | `web_analytics` | [Web analytics](https://posthog.com/docs/web-analytics) | | `workflows` | [Workflows](https://posthog.com/docs/workflows) | | `workspace` | Organization and project management | > **Note:** Hyphens and underscores are treated as equivalent in feature names (e.g., `error-tracking` and `error_tracking` both work). To view which tools are available per feature, see our [documentation](https://posthog.com/docs/model-context-protocol) or check `schema/tool-definitions-all.json`. ### Learning and skills in cli mode Claude web and desktop silently drop `exec` when its serialized `inputSchema` reaches 16,384 characters. In cli mode, the `posthog` tool keeps the guidance needed for routine calls in its schema. The compact tool-domain index stays inline in the `command` schema so Claude can discover relevant tools before making a call. Optional, task-specific guidance is served through the same tool: - `learn` lists the available built-in guides and, when enabled, the PostHog/project skill discovery syntax. - `learn analytics` loads detailed analytics guidance and examples. - `learn visualizations` loads rendering guidance when visualizations are available. - `learn urls` loads the PostHog app link formatting rules (kept inline for other clients; served as a guide on Claude web and desktop to protect the schema budget). - `learn feedback` loads feedback guidance when feedback is available. - `learn skills` lists qualified names from the published PostHog bundle (`posthog:`) and the current project's Skills store (`project:`). - `learn -s ""` searches both sources in names, descriptions, Markdown bodies, and bundled file paths. Longer keywords take priority when a query contains more than eight. Results from both sources are merged into one relevance order. - `learn -d : [...]` prints the one-line description of each named skill without reading its body (up to 20 per call). Unknown names are reported inline without failing the batch. - `learn posthog: [path]` or `learn project: [path]` reads a skill or one of its bundled files. - `learn : [path...]` reads several bundled files, and `learn : [:...]` reads several skills, in one call (up to 10 targets). - `learn : -s ""` searches within one Markdown file. - `learn : --lines :` reads an inclusive line range. Built-in guides are specific to Claude web and desktop. Skill discovery is independently available to every cli-mode client when the `mcp-exec-skills` feature flag is enabled. Other clients, including Claude Code, receive only the skill commands and do not receive Claude's built-in guides. If the flag is missing, disabled, or cannot be evaluated, skill commands are omitted from the schema and rejected at runtime. When skill discovery is enabled, the inline prompt tells every non-plugin cli client, including Claude web and desktop, to search with `learn -s ""` before non-trivial PostHog work, load matches by exact qualified name, and follow the loaded `SKILL.md` before choosing tools. Trivial lookups and unrelated conversation skip this workflow. Skill use is advisory: a product `call` is never rejected for skipping `learn`, so every client behaves the same whether or not it holds an MCP session id. The skill bundle is shared through Redis: each pod loads it once at startup, parses it into memory, and serves every `learn` command from that parsed catalog. A background timer polls a small version key in Redis; only when the version changes does a pod read the archive bytes again. One pod at a time refreshes the archive from its source when the shared copy is older than ten minutes, using a conditional request so an unchanged release costs a 304. The cached bytes are kept for thirty days and re-extended on every refresh, so a source outage serves the last good archive. No `learn`, `initialize`, or `discover` request reads the archive from Redis. By default it is loaded from `https://github.com/PostHog/posthog/releases/download/agent-skills-latest/skills.zip`. Set `POSTHOG_MCP_SKILLS_URL` to use another archive during local development. Custom archive URLs use separate Redis cache namespaces so a local bundle cannot read or overwrite the published bundle's cache entry. Project skills are read directly from the request-authenticated project and are not cached by the MCP server. Only latest, active, uncategorized skills are exposed through `learn`; category-specific skills such as scouts stay on their own surfaces. Project full-text search is bounded to 10 skills, two short snippets per skill, and a five-second database timeout. Individual `learn` responses stay below 44,000 characters; large references return a heading outline for follow-up search or line reads. The fixed command syntax stays in the tool schema, while skill names and bodies are loaded only when requested. `consumer=plugin` and `consumer=posthog-code` omit `learn` because both surfaces already supply their own bundled skill context, regardless of the feature flag. Other clients keep the full inline command reference. ### Tool filtering For finer-grained control you can allowlist specific tools by name using the `tools` query parameter. Only the exact tool names listed will be exposed, regardless of their feature category. ```text https://mcp.posthog.com/mcp?tools=dashboard-get,feature-flag-get-all,execute-sql ``` When `features` and `tools` are both provided they are combined as a **union** — a tool is included if it matches a feature category **or** is in the tools list. This lets you select a feature group and add a handful of individual tools on top: ```text https://mcp.posthog.com/mcp?features=flags&tools=dashboard-get ``` The example above exposes all flag tools plus `dashboard-get`. ### Server mode (tools vs cli) The MCP server can register either every PostHog tool individually (**tools** mode) or wrap them all behind a single `posthog` CLI-like tool (**cli** mode). **cli is the default for all clients.** When the caller does not pin a mode, the server only auto-selects tools mode for a short allow-list of clients that are better served by the full per-tool roster — currently Cursor (matched by its self-reported client name or its `Cursor/…` User-Agent). Every OpenAI surface (ChatGPT, Codex, Agent Builder, Responses API) gets the cli default. OpenAI's `openai-mcp` client caches the roster it captures for a published plugin and serves that snapshot to every user of the plugin, so the mode a plugin listing should run in is pinned on the URL submitted to OpenAI rather than inferred from a User-Agent label. You can pin the choice yourself with either a query parameter or a header. Only `tools` and `cli` are accepted: ```text https://mcp.posthog.com/mcp?mode=cli https://mcp.posthog.com/mcp?mode=tools ``` ```http x-posthog-mcp-mode: cli x-posthog-mcp-mode: tools ``` | Value | Behavior | | ------- | ------------------------------------------------------- | | `tools` | Force tools mode (one MCP tool per PostHog tool). | | `cli` | Force cli mode (single `posthog` tool wraps all tools). | The header wins when both the header and the query parameter are set. An explicit value always wins over the client auto-detection; any other value is ignored and the auto-detection takes over. The cli-mode command surface is documented publicly on [posthog.com/docs/model-context-protocol/tools](https://posthog.com/docs/model-context-protocol/tools), which embeds `schema/exec-command-reference.md` at build time. That fragment is generated from the templates in `src/templates/sections/` by `scripts/generate-exec-docs.ts` (part of `hogli build:openapi`); edit the templates, not the fragment. ### Consumer attribution Wrapping apps and AI-tool plugins that install or proxy the PostHog MCP can self-identify so usage can be attributed to the install path (e.g. plugin-installed vs. manually-pasted URL). The wrapped MCP client (Claude Code, Cursor, …) is already captured separately via the MCP `clientInfo` handshake — this signal is only for the wrapping context. ```text https://mcp.posthog.com/mcp?consumer=plugin ``` ```http x-posthog-mcp-consumer: plugin ``` The header wins when both the header and the query parameter are set. Reserved values: `plugin` (AI-tool plugin installs), `posthog-code` (PostHog Desktop Tasks sandbox), `slack` (Slack integration). ### Data processing The MCP server runs in PostHog's US and EU Kubernetes clusters and stores session state in the region you connect to. A stateless Cloudflare Worker in front of it only authenticates requests and routes them to your cloud region; it does not store any sensitive data. ### Using self-hosted instances If you're using a self-hosted instance of PostHog, you can specify a custom base URL by setting the `POSTHOG_API_BASE_URL` environment variable when running the MCP server locally or on your own infrastructure, e.g. `POSTHOG_API_BASE_URL=https://posthog.example.com` # Development To run the MCP server (Hono on Node) locally, run the following command: ```bash pnpm run dev ``` Or use `bin/start-mcp-server` from the repo root, which also bootstraps `.env` and sets Redis/port defaults. Then replace `https://mcp.posthog.com/mcp` with `http://localhost:8787/mcp` in the MCP configuration. The server defaults to port **8787**, reads config from `.env` (see `.env.example`), and expects a local Redis on port `6379` for session state; production deployments must set `REDIS_URL` to a TLS-encrypted `rediss://` endpoint. ### Session cache A session's client context lives in one `mcp:s::c` key with a 24-hour idle expiry, refreshed on every request in the session. Concurrent requests merge their fields through a Lua compare-and-merge, so a field first seen mid-session is never lost to an overlapping write. Monitor `mcp_session_cache_operations_total` for `read_error` and `write_error`. Both are non-blocking: a failed read serves whatever context the current request carries, so attribution degrades rather than the call failing. Each also logs a warning prefixed `[McpSessionRedisStore]`, so a Redis failure on this path is greppable in logs and not only visible on the metrics counter. ### Edge-proxy worker (Cloudflare) In production, a thin Cloudflare Worker sits in front of the Hono deployments as a stateless edge router: it serves the OAuth metadata endpoints, validates tokens, resolves the caller's cloud region, and proxies `/mcp` traffic to `mcp.us.posthog.com` / `mcp.eu.posthog.com`. It does not serve the MCP protocol itself - see [ARCHITECTURE.md](ARCHITECTURE.md). To run just the worker locally: ```bash pnpm run dev:proxy ``` ### Developing with local resources To develop with warm loading for MCP resources (workflows, prompts, examples): 1. Start the [context-mill](https://github.com/PostHog/context-mill) dev server: `cd ../context-mill && npm run dev` 2. Start the MCP server with local resources: `pnpm run dev:local-resources` (runs `bin/start-mcp-server` with `POSTHOG_MCP_LOCAL_SKILLS_URL` pointed at context-mill) Changes in the examples repo will be reflected on the next request. ## Project Structure - `src/` - The MCP server: Hono app (`src/hono/`), tool handlers (`src/tools/`), prompt templates (`src/templates/`) - `definitions/` - Hand-authored YAML tool definitions (per-product YAML lives at `products//mcp/` in the monorepo) - `schema/` - Generated schema files, including `tool-definitions-all.json` (the full tool catalog) - `typescript/` - A small shim (`typescript/src/tools/posthogAiTools/`) consumed by posthog-ai ### Development Commands - `pnpm run dev` - Start the MCP development server - `pnpm run dev:proxy` - Start the edge-proxy worker (wrangler) - `pnpm run lint` / `pnpm run format:check` - Verify linting and formatting without changing files - `pnpm run lint:fix` - Apply safe lint fixes without suggestion fixes - `pnpm run format` - Format code with Oxfmt only - `pnpm run fix` - Apply safe lint fixes, always format code, and report failures from either tool ### Adding New Tools See the [tools documentation](src/tools/README.md) for a guide on adding new tools to the MCP server. ### Environment variables Copy `.env.example` to `.env` in the root and adjust the values as needed. ### Configuring the Model Context Protocol Inspector During development you can directly inspect the MCP tool call results using the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector). You can run it using the following command: ```bash npx @modelcontextprotocol/inspector npx -y mcp-remote@latest http://localhost:8787/mcp --header "\"Authorization: Bearer {INSERT_YOUR_PERSONAL_API_KEY_HERE}\"" ``` Alternatively, you can use the following configuration in the MCP Inspector: Use transport type `STDIO`. **Command:** ```bash npx ``` **Arguments:** ```bash -y mcp-remote@latest http://localhost:8787/mcp --header "Authorization: Bearer {INSERT_YOUR_PERSONAL_API_KEY_HERE}" ``` ### Developing against Claude Desktop Claude Desktop is one of the easiest ways to test MCP Apps - while PostHog Desktop doesn't support it. You can configure access Settings > Developer and then edit `claude_desktop_config.json` with the following: ```json { "mcpServers": { "posthog-local": { "command": "npx", "args": ["-y", "mcp-remote@latest", "http://localhost:8787/mcp"] } } } ``` ## Privacy & Support - **Privacy Policy:** https://posthog.com/privacy - **Terms of Service:** https://posthog.com/terms - **Support:** https://posthog.com/questions or email support@posthog.com - **GitHub Issues:** https://github.com/PostHog/posthog/issues ### Data handling The MCP server acts as a proxy to your PostHog instance. It does not store your analytics data - all queries are executed against your PostHog project and results are returned directly to your AI client. Session state (active project/organization) is cached temporarily, keyed by your API key hash. For EU users, use the `mcp-eu.posthog.com` endpoint to ensure OAuth flows route to the EU PostHog instance.