npx skills add ...
npx skills add posthog/posthog --skill implementing-mcp-tools
Guide for exposing PostHog product endpoints as MCP tools. Use when creating new or updating API endpoints, adding MCP tool definitions, scaffolding YAML configs, or writing serializers with good descriptions. Covers the full pipeline from Django serializer to generated TypeScript tool handler.
npx skills add posthog/posthog --skill implementing-mcp-tools
Read the full guide at docs/published/handbook/engineering/ai/implementing-mcp-tools.md.
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:
help_text —
these flow all the way to Zod .describe() in the generated tool.
Missing descriptions = agents guessing at parameters.
Use ListField(child=serializers.CharField()) instead of bare ListField(),
and @extend_schema_field(PydanticModel) on JSONField subclasses to get typed Zod output
(see products/alerts/backend/api/alert.py for the pattern).ViewSet methods have @extend_schema(request=...) —
without it, drf-spectacular can't discover the request body
and the generated tool gets z.object({}) (zero parameters).
ModelViewSet with a serializer_class is fine; plain ViewSet with manual validation is not.@validated_request or @extend_schema with 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 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.
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.
Tool names and feature identifiers are validated at build time and in CI. Violations fail the build.
[a-z0-9-], no leading/trailing hyphensdomain-action, e.g. cohorts-create, dashboard-get, feature-flags-listThe single-exec prompt builds its compact domain index with
ToolDomainExtractor. Large families can split at an intermediate segment.
Without action trimming, experiment-freeze-exposure can advertise the
redundant domain experiment-freeze instead of experiment.
Whenever you add or rename an action tool, check the
TRAILING_ACTIONS set in the
same change. If a rendered domain can end in an operation verb that is not
already present, add the verb. Cover it in
services/mcp/tests/unit/instructions.test.ts. This applies even when the verb
is not the final segment of the full tool name.
Add operation verbs such as freeze, publish, or emit. Do not add resource
or capability nouns such as config, logs, stats, or schedule merely to
make the prompt shorter; those remain useful discovery domains.
[a-z0-9*], must start with a lettererror_tracking, feature_flagsMCP clients enforce different limits on tool names. The 52-char limit is the safe zone that works across all known clients:
| Client | Limit | Notes |
|---|---|---|
| MCP spec (draft) | 1–128 chars, [A-Za-z0-9_\-.] | Official recommendation, not enforced |
| Claude Code | 64 chars | Hard limit; prefixes tool names with mcp____ |
| Cursor | 60 chars combined | server_name + tool_name; tools over this are silently filtered |
| OpenAI API | ^[a-zA-Z0-9_-]+$, 64 chars | No dots allowed |
With the server name "posthog" (7 chars) plus a separator, tool names must stay at or below 52 characters to fit within Cursor's 60-char combined limit.
pnpm --filter=@posthog/mcp lint-tool-names — validates length and pattern for YAML and JSON definitionsTOOL_MAP and GENERATED_TOOL_MAP entriesYAML 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 folderThe build pipeline discovers YAML files from both paths.
Unknown keys are rejected at build time (Zod .strict()).
Add feature_flag to any tool (standard or query wrapper) to gate its exposure on a PostHog feature flag evaluated at MCP init time for the current user.
feature_flag_behavior: enable (default) — tool is shown only when the flag is on. Use for rolling out new tools.feature_flag_behavior: disable — tool is hidden when the flag is on. Use for sunsetting old tools.Reusing the same flag key with both behaviors performs an atomic swap: flag on → new tool visible, old tool hidden; flag off → old tool visible, new tool hidden. Useful for A/B testing tool variations.
Flags are evaluated in parallel at init via evaluateFeatureFlags. If a flag can't be evaluated (service error, missing flag), enable-gated tools are excluded and disable-gated tools are included — fail-closed for new tools, fail-open for existing ones.
Idempotent and non-destructive — adds new operations as enabled: false, removes stale ones.
Descriptions flow through the entire pipeline:
These descriptions are what agents read to understand tool parameters.
help_text on serializer fields — it becomes the OpenAPI description.param_overrides in YAML to override generated descriptions with imperative instructions.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/querying-posthog-data/references/
and register it in products/posthog_ai/skills/querying-posthog-data/SKILL.md under Data Schema.
Control per-tool availability with mcp_version: 1/2 in the YAML definition.
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
response: # filter response fields (applied per-item on list endpoints)
include: [id, key, name] # keep only these fields (dot-path wildcards supported)
exclude: [filters.groups.*.properties] # remove these fields
# include and exclude are mutually exclusive
selectable: true # add optional `fields` param so the agent picks a subset of `include` per call
# (constrained to the allowlist); omit `fields` to return the full set. Requires `include`.
strip_nulls: true # remove keys whose value is `null`, applied after include/exclude
# Use it on tools that echo a nested serializer schema, where the unset optional fields
# dominate the payload. Rejected with `list: true`, where per-row null removal makes the
# TOON table larger. Use `exclude` to drop the fields on a list tool instead.
feature_flag: my-flag-key # gate this tool behind a PostHog feature flag
feature_flag_behavior: enable # 'enable' (default) or 'disable'pnpm --filter=@posthog/mcp run scaffold-yaml -- --sync-allDjango serializer field → OpenAPI spec → Zod schema → MCP tool description