implementing-mcp-tools
Implementing MCP tools
Read the full guide at docs/published/handbook/engineering/ai/implementing-mcp-tools.md.
Quick workflow
# 1. Scaffold a starter YAML with all operations disabled.
# --product discovers endpoints via x-explicit-tags (priority 1) then
# URL substring match (fallback). ViewSets in products/<name>/backend/
# are auto-tagged. ViewSets elsewhere need @extend_schema(tags=["<product>"]).
pnpm --filter=@posthog/mcp run scaffold-yaml -- --product your_product \
--output ../../products/your_product/mcp/tools.yaml
# 2. Configure the YAML — enable tools, add scopes, annotations, descriptions
# Place in products/<product>/mcp/*.yaml (preferred) or services/mcp/definitions/*.yaml
# 3. Add a HogQL system table in posthog/hogql/database/schema/system.py
# and a model reference in products/posthog_ai/skills/query-examples/references/
# 4. Generate handlers and schemas
hogli build:openapi
Before you scaffold: fix the backend first
The codegen pipeline can only generate correct tools if the Django backend exposes correct types. Read the type system guide for the full picture.
Before scaffolding YAML, verify:
- Serializers have explicit field types and
help_text— these flow all the way to Zod.describe()in the generated tool. Missing descriptions = agents guessing at parameters. UseListField(child=serializers.CharField())instead of bareListField(), and@extend_schema_field(PydanticModel)onJSONFieldsubclasses to get typed Zod output (seeposthog/api/alert.pyfor the pattern). - Plain
ViewSetmethods have@extend_schema(request=...)— without it, drf-spectacular can't discover the request body and the generated tool getsz.object({})(zero parameters).ModelViewSetwith aserializer_classis fine; plainViewSetwith manual validation is not. - Query parameters use
@validated_requestor@extend_schemawith a query serializer — otherwise boolean and array query params may produce type mismatches in the generated code.
If a generated tool has an empty or wrong schema, the fix is almost always on the Django side,
not in the YAML config.
For a full audit checklist and before/after examples, use the improving-drf-endpoints skill.
When to add MCP tools
When a product exposes API endpoints that agents should be able to call. MCP tools are atomic capabilities (list, get, create, update, delete) — not workflows.
If you're adding a new endpoint, check whether it should be agent-accessible. If yes, add a YAML definition and generate the tool.
Tool design
Tools should be basic capabilities — atomic CRUD operations and simple actions. Agents compose these primitives into higher-level workflows.
Good: "List feature flags", "Get experiment by ID", "Create a survey". Bad: "Search for session recordings of an experiment" — bundles multiple concerns.
YAML definitions
YAML files configure which operations are exposed as MCP tools. See existing definitions for patterns:
products/<product>/mcp/*.yaml— preferred, keeps config close to the codeservices/mcp/definitions/*.yaml— fallback for functionality without a product folder
The build pipeline discovers YAML files from both paths.
Key fields
category: Human readable name
feature: snake_case_name # should match the product folder name (used for runtime filtering)
url_prefix: /path # frontend app route, used for enrich_url links
tools:
your-tool-name: # kebab-case
operation: operationId_from_openapi
enabled: true
scopes:
- your_product:read
annotations:
readOnly: true
destructive: false
idempotent: true
# Optional:
mcp_version: 1 # 2 for create/update/delete ops, 1 for read/list if available via HogQL
title: List things
description: >
Human-friendly description for the LLM.
list: true
enrich_url: '{id}'
param_overrides:
name:
description: Custom description for the LLM
Unknown keys are rejected at build time (Zod .strict()).
Syncing after endpoint changes
pnpm --filter=@posthog/mcp run scaffold-yaml -- --sync-all
Idempotent and non-destructive — adds new operations as enabled: false, removes stale ones.
Serializer descriptions
Descriptions flow through the entire pipeline:
Django serializer field → OpenAPI spec → Zod schema → MCP tool description
These descriptions are what agents read to understand tool parameters.
- Use
help_texton serializer fields — it becomes the OpenAPI description. - Use
param_overridesin YAML to override generated descriptions with imperative instructions. - Be specific about formats, constraints, and valid values.
- Avoid jargon that an LLM wouldn't understand without context.
HogQL system tables
Every list/get endpoint should have a corresponding HogQL system table
in posthog/hogql/database/schema/system.py.
This lets agents query data via SQL in v2 of the MCP.
Each system table must include a team_id column for data isolation.
Use mcp_version: 1 on read/list YAML tools when a system table covers the same data —
v2 agents use SQL instead.
When adding a system table, also add a model reference file
(models-<domain>.md) in products/posthog_ai/skills/query-examples/references/
and register it in products/posthog_ai/skills/query-examples/SKILL.md under Data Schema.
Two MCP versions
- v1 (legacy): all CRUD tools exposed, for clients without skill support.
- v2 (SQL-first): read/list tools replaced by HogQL, create/update/delete tools kept. For coding agents.
Control per-tool availability with mcp_version: 1/2 in the YAML definition.