Implementing MCP tools
Contents
MCP tools are atomic capabilities – CRUD operations and simple actions that agents compose into workflows. Every product should be accessible through the MCP server. Tools answer "what can I do?" (list feature flags, execute SQL, create a survey).
For teaching agents how to use these capabilities in combination, see Writing skills.
TL;DR
Tool design principles
MCP tools should be basic capabilities – atomic CRUD operations and simple actions. Agents compose these primitives into higher-level workflows.
Good tools:
- List feature flags
- Get an experiment by ID
- Create a survey
- Summarize a session recording
Bad tools:
- "Search for session recordings of an experiment" – this bundles multiple concerns. Instead, expose four composable tools: list experiments, get experiment, search session recordings, summarize sessions.
The reasoning: agents are better at composing simple tools than navigating complex ones, and simple tools are reusable across many workflows.
Two MCP server versions
Clients must support two main capabilities: MCPs and skills.
MCP support is widespread; however, skills support is still very early
and mostly coding agents support them.
To mitigate this, the MCP server ships two versions controlled via the
x-posthog-mcp-version: <version_number> header.
Legacy MCP (v1)
For clients that don't support skills. Exposes the full set of CRUD tools with simple instructions (list, read, create, update, delete).
Primarily oriented toward vibe-coding web tools.
SQL-first MCP for clients supporting skills (v2)
v2 instructs the agent to read data through a unified HogQL interface (list and get tools are generally excluded), which unlocks flexibility in data retrieval, search, and manipulation. Additionally, the consumer has access to a skill that provides schema references and example patterns, giving it richer context about PostHog's data model.
Primarily oriented toward coding agents (PostHog Code, PostHog AI, Claude Code).
SQL-first MCP: HogQL system tables
Every list/get endpoint exposed as an MCP tool must have a corresponding HogQL system table. This lets agents query PostHog data via SQL in addition to (or instead of) the REST API tools.
System tables are defined in posthog/hogql/database/schema/system.py as PostgresTable instances.
Each table must include a team_id column for data isolation.
Use mcp_version: 1/2 to control availability of retrieval tools in v2 of the MCP.
Example from the codebase:
Agents query these tables with the system. prefix:
Extending query examples
When you add a new system table,
also add a model reference file to products/posthog_ai/skills/query-examples/references/.
The naming convention is models-<domain>.md.
Existing references:
models-actions.mdmodels-cohorts.mdmodels-dashboards-insights.mdmodels-data-warehouse.mdmodels-error-tracking.mdmodels-flags-experiments.mdmodels-groups.mdmodels-notebooks.mdmodels-surveys.mdmodels-variables.md
Each file documents the table's columns, types, nullability, and notable structures (like JSON fields).
See models-flags-experiments.md for a good example.
Register your new reference in products/posthog_ai/skills/query-examples/SKILL.md under Data Schema.
Code generation pipeline
The pipeline turns Django serializers into MCP tool handlers via OpenAPI. Run the full pipeline with:
Pipeline steps
YAML definitions
YAML definitions are the configuration layer.
They live in products/<product>/mcp/*.yaml, keeping config close to the owning product's code.
Fallback path:
services/mcp/definitions/*.yamlis available for functionality that doesn't have a product folder. When a product folder exists, always place definitions there.
The build pipeline discovers YAML files from both paths. Product teams own their definitions and control which operations are exposed as MCP tools.
Workflow: scaffold, configure, generate.
Scaffold a starter YAML with all operations disabled.
--productdiscovers endpoints in two ways (same priority as frontend type generation):x-explicit-tags— matches endpoints whose OpenAPI tag equals the product name. ViewSets inproducts/<name>/backend/are auto-tagged. ViewSets elsewhere (e.g.posthog/api/) need@extend_schema(tags=["<product>"]).- URL substring fallback — selects endpoints whose path contains
/<name>/(hyphens normalized to underscores).
If your product's API routes use a different slug than the product folder name (e.g.
workflowsproduct with/hog_flows/routes), add@extend_schema(tags=["workflows"])to the ViewSet so the scaffold can find them.shConfigure the YAML – enable tools, add scopes, annotations, and descriptions. Each YAML file has a top-level structure validated by Zod (
scripts/yaml-config-schema.ts):Tool names follow a
domain-actionconvention in kebab-case, e.g.feature-flags-list,experiments-create,surveys-delete. The domain groups related tools together and the action describes the operation.Tool name length limit: Some MCP clients (notably Cursor) enforce a 60-character combined limit on
server_name:tool_name. Since our server name isposthog(7 chars), tool names must be 52 characters or fewer. CI runspnpm --filter=@posthog/mcp lint-tool-namesto enforce this. If you hit the limit, shorten the domain prefix or use a more concise action name.YAMLUnknown keys are rejected at build time (Zod
.strict()) to catch typos early.Custom input schemas
By default, tool input schemas are auto-derived from OpenAPI via Orval. When the auto-derived schema isn't ideal for an LLM tool interface, you can override it at two levels:
Whole-tool override — set
input_schemaon the tool to a named export fromsrc/schema/tool-inputs.ts. The generated handler imports that schema instead of composing Orval imports. Theoperationis still used for the HTTP method and path. Path parameters are extracted from the URL pattern; remaining parameters are forwarded as body (POST/PATCH/PUT) or query (GET/DELETE).Per-param override — set
input_schemainsideparam_overridesto replace a single field's Zod type while keeping the rest of the Orval-derived schema. The generated code uses.extend()to replace just that field. See supported annotations for the full list.Generate handlers and schemas:
sh
Keeping definitions in sync
When backend API endpoints change, sync the YAML definitions:
This is idempotent and non-destructive –
it only adds newly discovered operations (with enabled: false) and removes stale ones.
All hand-authored configuration is preserved.
CI runs this as a drift check.
See services/mcp/definitions/README.md for the full YAML schema reference (note: YAML definitions themselves now live in product folders)
and services/mcp/scripts/yaml-config-schema.ts for the Zod validation source.
Testing
See How to develop and test for instructions on running the MCP server locally and verifying tools end-to-end.
Serializer best practices
Descriptions flow through the entire pipeline:
Product teams should type and describe their serializer fields. These descriptions are what agents read to understand tool parameters – vague or missing descriptions lead to worse agent behavior.
See the type system guide for the full backend → frontend pipeline,
including how to set up viewsets, serializers, and @extend_schema correctly.
For a comprehensive audit checklist, before/after examples, and detailed serializer/viewset patterns,
see the improving-drf-endpoints skill.
Tips:
- Use
help_texton serializer fields – it becomes the OpenAPI description. Be careful when using imperative language inhelp_text, as the same annotations are used in the API docs. - Use
param_overridesin YAML definitions to override Orval-generated descriptions. This is useful when you want to add imperative instructions for specific fields. - Be specific about formats, constraints, and valid values.
- Avoid jargon that an LLM wouldn't understand without context.
ListFieldandJSONFieldneed explicit types — useListField(child=serializers.CharField())instead of bareListField(), and@extend_schema_field(PydanticModel)onJSONFieldsubclasses (seeposthog/api/alert.pyfor the pattern). Without this, Orval generatesz.unknown().- Plain
ViewSetmethods that validate manually need@extend_schema(request=YourSerializer)— without it, drf-spectacular can't discover the request body and the generated tool gets an empty schema with zero parameters.ModelViewSetwithserializer_classworks automatically.
HogQL query schemas (WIP)
frontend/src/queries/schema/schema-assistant-queries.ts defines structured query types
for the AI assistant (trends, funnels, retention, etc.).
These schemas describe the shape of analytical queries with rich JSDoc comments that help agents generate correct HogQL. The cleaner and better-described these schemas are, the better agents perform at query generation.
This is a work in progress –
the goal is to make it easier to generate HogQL queries from typed schemas
than from freeform SQL.
A schema.json integration into the codegen pipeline is planned.
Agent skills that support the MCP server
query-examples– HogQL query patterns, system model schemas, and available functions. Extend this skill to explain how agents should use your HogQL-exposed tables and queries. Seeproducts/posthog_ai/skills/query-examples/SKILL.md.improving-drf-endpoints– Audit checklist and patterns for DRF serializers and viewsets. Use when editing or reviewing endpoints to ensurehelp_text, field types, and@extend_schemaannotations flow correctly through the type pipeline. See.agents/skills/improving-drf-endpoints/SKILL.md.