implementing-mcp-tools

Guía para exponer los endpoints del producto PostHog como herramientas MCP. Úsalo al crear nuevos endpoints de API o actualizarlos, agregar definiciones de herramientas MCP, estructurar YAML…

npx skills add https://github.com/posthog/posthog --skill 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 their x-product attribution.
#    ViewSets in products/<name>/backend/ are auto-attributed via module
#    path. ViewSets elsewhere need
#    @extend_schema(extensions={"x-product": "<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/querying-posthog-data/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:

  1. 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. 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/presentation/views/alert.py for the pattern).
  2. Plain 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.
  3. Query parameters use @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 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.

Tool naming constraints

Tool names and feature identifiers are validated at build time and in CI. Violations fail the build.

Tool names

  • Format: lowercase kebab-case — only [a-z0-9-], no leading/trailing hyphens
  • Length: 52 characters or fewer
  • Convention: domain-action, e.g. cohorts-create, dashboard-get, feature-flags-list

Keep action verbs out of compact tool domains

The 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.

Feature identifiers

  • Format: lowercase snake*case — only [a-z0-9*], must start with a letter
  • Convention: should match the product folder name, e.g. error_tracking, feature_flags

Why 52 characters?

MCP clients enforce different limits on tool names. The 52-char limit is the safe zone that works across all known clients:

ClientLimitNotes
MCP spec (draft)1–128 chars, [A-Za-z0-9_\-.]Official recommendation, not enforced
Claude Code64 charsHard limit; prefixes tool names with mcp____
Cursor60 chars combinedserver_name + tool_name; tools over this are silently filtered
OpenAI API^[a-zA-Z0-9_-]+$, 64 charsNo 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.

CI enforcement

  • pnpm --filter=@posthog/mcp lint-tool-names — validates length and pattern for YAML and JSON definitions
  • A vitest test validates all runtime TOOL_MAP and GENERATED_TOOL_MAP entries

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 code
  • services/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
    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'

Unknown keys are rejected at build time (Zod .strict()).

Gating tools with feature flags

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.

Enabling or renaming a tool

The MCP server and Django deploy separately. A tool that reaches clients before its route lands returns 404 on every call until the Django deploy catches up. That hits a whole agent fleet at once.

  • Land the route first. Ship the endpoint, then enable the tool in a later change. A tool with enabled: true in the same commit as a brand-new route is live in clients as soon as the MCP server deploys.
  • Or gate it. Add feature_flag with feature_flag_behavior: enable and turn the flag on once the route is serving.
  • Keep the old name on a rename. Leave the previous tool name in the YAML, pointing at the same operation, until the new name has deployed everywhere. Sunset it with feature_flag_behavior: disable on the same flag key, which swaps the two atomically.

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_text on serializer fields — it becomes the OpenAPI description.
  • Use param_overrides in 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/querying-posthog-data/references/ and register it in products/posthog_ai/skills/querying-posthog-data/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.

Más skills de posthog

error-tracking-hono
posthog
Seguimiento de errores de PostHog para Hono
tuning-incremental-sync-config
posthog
La configuración de una sincronización reside en ExternalDataSchema y puede modificarse en cualquier momento mediante external-data-schemas-partial-update. La mayoría de los cambios no son destructivos (entran en vigor en la siguiente sincronización), pero algunos (cambiar sync_type, modificar claves primarias) requieren un manejo cuidadoso para evitar corromper los datos sincronizados.
playwright-test
posthog
Escribe una prueba de Playwright, asegúrate de que se ejecute y no sea inestable.
error-tracking-ruby
posthog
Seguimiento de errores de PostHog para Ruby
authoring-log-alerts
posthog
Crea alertas de logs útiles y de bajo ruido en los servicios de un proyecto de PostHog. Úsalo cuando el usuario pida configurar alertas para sus logs, sugerir alertas que debería añadir,…
making-scenes-tab-aware
posthog
Guides converting PostHog frontend scenes to be tab aware for internal scene tabs. Use when adding or refactoring a `SceneExport` scene, fixing state leaking…
posthog-survey-creator
posthog
Crear y configurar encuestas en PostHog mediante una conversación guiada. Usa esta habilidad cuando un usuario quiera crear una encuesta, recopilar comentarios de usuarios, ejecutar…
authoring-scouts
posthog
Cómo redactar, editar y adaptar los scouts de PostHog Signals — los agentes programados que escanean un proyecto y escriben informes en la bandeja de entrada de Signals. Úselo cuando un usuario…